From cd69060b26f0b09cf20727ef0dec62416f6212e0 Mon Sep 17 00:00:00 2001 From: eytanadler Date: Thu, 11 Aug 2022 09:27:44 -0400 Subject: [PATCH 1/7] Ran black --- docs/_exts/docutil_legacy.py | 236 +-- docs/_exts/embed_bibtex.py | 11 +- docs/_exts/embed_code.py | 171 ++- docs/_exts/embed_compare.py | 46 +- docs/_exts/embed_n2.py | 20 +- docs/_exts/embed_options.py | 9 +- docs/_exts/embed_shell_cmd.py | 46 +- docs/_exts/link_class_from_docstring.py | 28 +- docs/_exts/tags.py | 28 +- docs/conf.py | 162 +- openconcept/__init__.py | 2 +- openconcept/aerodynamics/aerodynamics.py | 159 +- .../openaerostruct/aerostructural.py | 13 +- .../aerodynamics/openaerostruct/drag_polar.py | 10 +- .../tests/test_aerostructural.py | 9 +- .../aerodynamics/tests/test_aerodynamics.py | 91 +- openconcept/atmospherics/atmospherics_data.py | 72 +- .../atmospherics/compute_atmos_props.py | 60 +- openconcept/atmospherics/density_comp.py | 36 +- .../atmospherics/dynamic_pressure_comp.py | 28 +- openconcept/atmospherics/mach_number_comp.py | 27 +- openconcept/atmospherics/pressure_comp.py | 31 +- openconcept/atmospherics/speedofsound_comp.py | 27 +- openconcept/atmospherics/temperature_comp.py | 35 +- .../atmospherics/tests/test_atmospherics.py | 109 +- openconcept/atmospherics/true_airspeed.py | 54 +- openconcept/costs/__init__.py | 2 +- openconcept/costs/costs_commuter.py | 195 ++- openconcept/energy_storage/battery.py | 170 +- .../energy_storage/tests/test_battery.py | 89 +- openconcept/examples/B738.py | 254 +-- openconcept/examples/B738_VLM_drag.py | 293 ++-- openconcept/examples/B738_aerostructural.py | 485 +++--- openconcept/examples/Caravan.py | 282 ++-- .../examples/ElectricSinglewithThermal.py | 316 ++-- openconcept/examples/HybridTwin.py | 612 +++++--- .../examples/HybridTwin_active_thermal.py | 703 +++++---- openconcept/examples/HybridTwin_thermal.py | 750 +++++---- openconcept/examples/KingAirC90GT.py | 286 ++-- .../examples/N3_HybridSingleAisle_Refrig.py | 977 +++++++----- openconcept/examples/aircraft_data/B738.py | 66 +- .../aircraft_data/HybridSingleAisle.py | 165 +- .../examples/aircraft_data/KingAirC90GT.py | 90 +- openconcept/examples/aircraft_data/TBM850.py | 78 +- openconcept/examples/aircraft_data/caravan.py | 78 +- openconcept/examples/minimal.py | 13 +- openconcept/examples/minimal_integrator.py | 15 +- .../examples/tests/test_example_aircraft.py | 152 +- openconcept/mission/mission_groups.py | 270 ++-- openconcept/mission/phases.py | 1160 +++++++++----- openconcept/mission/profiles.py | 626 +++++--- .../tests/test_solver_phase_helpers.py | 259 ++-- .../mission/tests/test_trajectories.py | 801 +++++----- openconcept/propulsion/N3.py | 519 ++++--- openconcept/propulsion/cfm56.py | 105 +- .../propulsion/empirical_data/prop_maps.py | 240 ++- openconcept/propulsion/generator.py | 88 +- openconcept/propulsion/motor.py | 99 +- openconcept/propulsion/propeller.py | 316 ++-- openconcept/propulsion/splitter.py | 165 +- .../propulsion/systems/simple_all_electric.py | 326 ++-- .../systems/simple_series_hybrid.py | 389 ++--- .../propulsion/systems/simple_turboprop.py | 126 +- .../systems/thermal_series_hybrid.py | 590 +++---- openconcept/propulsion/tests/test_N3.py | 119 +- openconcept/propulsion/tests/test_cfm56.py | 58 +- .../propulsion/tests/test_simple_comps.py | 331 ++-- .../propulsion/tests/test_splitter_comps.py | 70 +- openconcept/propulsion/turboshaft.py | 106 +- openconcept/thermal/battery_cooling.py | 213 +-- openconcept/thermal/chiller.py | 271 ++-- openconcept/thermal/ducts.py | 967 +++++++----- openconcept/thermal/heat_exchanger.py | 1364 ++++++++++------- openconcept/thermal/heat_pipe.py | 678 +++++--- openconcept/thermal/hose.py | 127 +- openconcept/thermal/manifold.py | 98 +- openconcept/thermal/motor_cooling.py | 205 ++- openconcept/thermal/pump.py | 84 +- .../thermal/tests/test_battery_cooling.py | 282 +++- openconcept/thermal/tests/test_chiller.py | 163 +- openconcept/thermal/tests/test_ducts.py | 567 ++++--- .../thermal/tests/test_heat_exchanger.py | 591 +++---- openconcept/thermal/tests/test_heat_pipe.py | 205 +-- openconcept/thermal/tests/test_hose.py | 47 +- openconcept/thermal/tests/test_manifold.py | 62 +- .../thermal/tests/test_motor_cooling.py | 157 +- openconcept/thermal/tests/test_pump.py | 41 +- .../thermal/tests/test_thermal_comps.py | 141 +- openconcept/thermal/thermal.py | 294 ++-- openconcept/utilities/dict_indepvarcomp.py | 24 +- openconcept/utilities/dvlabel.py | 3 +- openconcept/utilities/linearinterp.py | 28 +- .../utilities/math/add_subtract_comp.py | 93 +- .../utilities/math/combine_split_comp.py | 216 +-- openconcept/utilities/math/derivatives.py | 169 +- openconcept/utilities/math/integrals.py | 914 ++++++----- openconcept/utilities/math/max_min_comp.py | 59 +- .../utilities/math/multiply_divide_comp.py | 157 +- .../math/tests/test_add_subtract_comp.py | 362 +++-- .../math/tests/test_combine_split.py | 660 ++++---- .../utilities/math/tests/test_integrals.py | 550 ++++--- openconcept/utilities/math/tests/test_math.py | 271 ++-- .../utilities/math/tests/test_min_max_comp.py | 100 +- .../math/tests/test_multiply_divide_comp.py | 479 +++--- .../math/tests/test_old_integrals.py | 412 +++-- openconcept/utilities/selector.py | 54 +- .../utilities/tests/test_dict_indepvarcomp.py | 75 +- openconcept/utilities/tests/test_dvlabel.py | 131 +- openconcept/utilities/tests/test_selector.py | 95 +- openconcept/utilities/visualization.py | 39 +- openconcept/weights/weights_turboprop.py | 873 +++++++---- openconcept/weights/weights_twin_hybrid.py | 67 +- setup.py | 54 +- 113 files changed, 15667 insertions(+), 11129 deletions(-) diff --git a/docs/_exts/docutil_legacy.py b/docs/_exts/docutil_legacy.py index fe348f91..b7897f0d 100644 --- a/docs/_exts/docutil_legacy.py +++ b/docs/_exts/docutil_legacy.py @@ -29,15 +29,15 @@ from openmdao.utils.general_utils import printoptions -sqlite_file = 'feature_docs_unit_test_db.sqlite' # name of the sqlite database file -table_name = 'feature_unit_tests' # name of the table to be queried +sqlite_file = "feature_docs_unit_test_db.sqlite" # name of the sqlite database file +table_name = "feature_unit_tests" # name of the table to be queried -_sub_runner = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'run_sub.py') +_sub_runner = os.path.join(os.path.dirname(os.path.abspath(__file__)), "run_sub.py") # an input block consists of a block of code and a tag that marks the end of any # output from that code in the output stream (via inserted print('>>>>>#') statements) -InputBlock = namedtuple('InputBlock', 'code tag') +InputBlock = namedtuple("InputBlock", "code tag") class skipped_or_failed_node(nodes.Element): @@ -53,7 +53,9 @@ def depart_skipped_or_failed_node(self, node): self.body.append("output only available for HTML\n") return - html = '
{}
'.format(node["kind"], node["text"]) + html = '
{}
'.format( + node["kind"], node["text"] + ) self.body.append(html) @@ -76,7 +78,9 @@ def depart_in_or_out_node(self, node): if node["kind"] == "In": html = '
{}
'.format(node["text"]) elif node["kind"] == "Out": - html = '
{}
'.format(node["text"]) + html = '
{}
'.format( + node["text"] + ) self.body.append(html) @@ -118,7 +122,7 @@ def remove_docstrings(source): if start_line > last_lineno: last_col = 0 if start_col > last_col: - out += (" " * (start_col - last_col)) + out += " " * (start_col - last_col) # This series of conditionals removes docstrings: if token_type == tokenize.STRING: if prev_toktype != tokenize.INDENT: @@ -165,7 +169,7 @@ def remove_redbaron_node(node, index): try: node.value.remove(node.value[index]) except Exception as e: # no choice but to catch the general Exception - if str(e).startswith('It appears that you have indentation in your CommaList'): + if str(e).startswith("It appears that you have indentation in your CommaList"): pass else: raise @@ -192,8 +196,15 @@ def replace_asserts_with_prints(src): rb = RedBaron(src) # convert to RedBaron internal structure # findAll is slow, so only check the ones that are present. - base_assert = ['assertAlmostEqual', 'assertLess', 'assertGreater', 'assertEqual', - 'assert_equal_arrays', 'assertTrue', 'assertFalse'] + base_assert = [ + "assertAlmostEqual", + "assertLess", + "assertGreater", + "assertEqual", + "assert_equal_arrays", + "assertTrue", + "assertFalse", + ] used_assert = [item for item in base_assert if item in src] for assert_type in used_assert: @@ -201,13 +212,13 @@ def replace_asserts_with_prints(src): for assert_node in assert_nodes: assert_node = assert_node.parent remove_redbaron_node(assert_node, 0) # remove 'self' from the call - assert_node.value[0].replace('print') - if assert_type not in ['assertTrue', 'assertFalse']: + assert_node.value[0].replace("print") + if assert_type not in ["assertTrue", "assertFalse"]: # remove the expected value argument remove_redbaron_node(assert_node.value[1], 1) - if 'assert_rel_error' in src: - assert_nodes = rb.findAll("NameNode", value='assert_rel_error') + if "assert_rel_error" in src: + assert_nodes = rb.findAll("NameNode", value="assert_rel_error") for assert_node in assert_nodes: assert_node = assert_node.parent # If relative error tolerance is specified, there are 4 arguments @@ -220,8 +231,8 @@ def replace_asserts_with_prints(src): # assert_node.value[0].replace("print") - if 'assert_near_equal' in src: - assert_nodes = rb.findAll("NameNode", value='assert_near_equal') + if "assert_near_equal" in src: + assert_nodes = rb.findAll("NameNode", value="assert_near_equal") for assert_node in assert_nodes: assert_node = assert_node.parent # If relative error tolerance is specified, there are 3 arguments @@ -231,8 +242,8 @@ def replace_asserts_with_prints(src): remove_redbaron_node(assert_node.value[1], -1) # remove the expected value assert_node.value[0].replace("print") - if 'assert_almost_equal' in src: - assert_nodes = rb.findAll("NameNode", value='assert_almost_equal') + if "assert_almost_equal" in src: + assert_nodes = rb.findAll("NameNode", value="assert_almost_equal") for assert_node in assert_nodes: assert_node = assert_node.parent # If relative error tolerance is specified, there are 3 arguments @@ -252,7 +263,7 @@ def remove_initial_empty_lines(source): directive for including source code into feature doc files. """ - idx = re.search(r'\S', source, re.MULTILINE).start() + idx = re.search(r"\S", source, re.MULTILINE).start() return source[idx:] @@ -283,10 +294,10 @@ class or None class_obj = None method_obj = None - if path.endswith('.py'): + if path.endswith(".py"): if not os.path.isfile(path): raise SphinxError("Can't find file '%s' cwd='%s'" % (path, os.getcwd())) - with open(path, 'r') as f: + with open(path, "r") as f: source = f.read() module = None else: @@ -299,9 +310,9 @@ class or None # Second, assume class and see if it works try: - parts = path.split('.') + parts = path.split(".") - module_path = '.'.join(parts[:-1]) + module_path = ".".join(parts[:-1]) module = importlib.import_module(module_path) class_name = parts[-1] class_obj = getattr(module, class_name) @@ -311,7 +322,7 @@ class or None except ImportError: # else assume it is a path to a method - module_path = '.'.join(parts[:-2]) + module_path = ".".join(parts[:-2]) module = importlib.import_module(module_path) class_name = parts[-2] method_name = parts[-1] @@ -332,7 +343,7 @@ def remove_raise_skip_tests(src): raise_nodes = rb.findAll("RaiseNode") for rn in raise_nodes: # only the raise for SkipTest - if rn.value[:2].dumps() == 'unittestSkipTest': + if rn.value[:2].dumps() == "unittestSkipTest": rn.parent.value.remove(rn) return rb.dumps() @@ -360,7 +371,7 @@ def remove_leading_trailing_whitespace_lines(src): imin = min(non_whitespace_lines) imax = max(non_whitespace_lines) - return '\n'.join(lines[imin: imax+1]) + return "\n".join(lines[imin : imax + 1]) def is_output_node(node): @@ -377,21 +388,23 @@ def is_output_node(node): bool True if node may be expected to generate output, otherwise False. """ - if node.type == 'print': + if node.type == "print": return True # lines with the following signatures and function names may generate output - output_signatures = [ - ('name', 'name', 'call'), - ('name', 'name', 'name', 'call') - ] + output_signatures = [("name", "name", "call"), ("name", "name", "name", "call")] output_functions = [ - 'setup', 'run_model', 'run_driver', - 'check_partials', 'check_totals', - 'list_inputs', 'list_outputs', 'list_problem_vars' + "setup", + "run_model", + "run_driver", + "check_partials", + "check_totals", + "list_inputs", + "list_outputs", + "list_problem_vars", ] - if node.type == 'atomtrailers' and len(node.value) in (3, 4): + if node.type == "atomtrailers" and len(node.value) in (3, 4): sig = [] for val in node.value: sig.append(val.type) @@ -422,7 +435,7 @@ def split_source_into_input_blocks(src): for line in src.splitlines(): if 'print(">>>>>' in line: tag = line.split('"')[1] - code = '\n'.join(current_block) + code = "\n".join(current_block) input_blocks.append(InputBlock(code, tag)) current_block = [] else: @@ -430,8 +443,8 @@ def split_source_into_input_blocks(src): if len(current_block) > 0: # final input block, with no associated output - code = '\n'.join(current_block) - input_blocks.append(InputBlock(code, '')) + code = "\n".join(current_block) + input_blocks.append(InputBlock(code, "")) return input_blocks @@ -450,61 +463,61 @@ def insert_output_start_stop_indicators(src): str String with output demarked. """ - lines = src.split('\n') + lines = src.split("\n") print_producing = [ - 'print(', - '.setup(', - '.run_model(', - '.run_driver(', - '.check_partials(', - '.check_totals(', - '.list_inputs(', - '.list_outputs(', - '.list_sources(', - '.list_source_vars(', - '.list_problem_vars(', - '.list_cases(', - '.list_model_options(', - '.list_solver_options(', + "print(", + ".setup(", + ".run_model(", + ".run_driver(", + ".check_partials(", + ".check_totals(", + ".list_inputs(", + ".list_outputs(", + ".list_sources(", + ".list_source_vars(", + ".list_problem_vars(", + ".list_cases(", + ".list_model_options(", + ".list_solver_options(", ] newlines = [] input_block_number = 0 in_try = False in_continuation = False - head_indent = '' + head_indent = "" for line in lines: newlines.append(line) # Check if we are concluding a continuation line. if in_continuation: line = line.rstrip() - if not (line.endswith(',') or line.endswith('\\') or line.endswith('(')): + if not (line.endswith(",") or line.endswith("\\") or line.endswith("(")): newlines.append('%sprint(">>>>>%d")' % (head_indent, input_block_number)) input_block_number += 1 in_continuation = False # Don't print if we are in a try block. if in_try: - if 'except' in line: + if "except" in line: in_try = False continue - if 'try:' in line: + if "try:" in line: in_try = True continue # Searching for 'print(' is a little ambiguous. - if 'set_solver_print(' in line: + if "set_solver_print(" in line: continue for item in print_producing: if item in line: - indent = ' ' * (len(line) - len(line.lstrip())) + indent = " " * (len(line) - len(line.lstrip())) # Line continuations are a litle tricky. line = line.rstrip() - if line.endswith(',') or line.endswith('\\') or line.endswith('('): + if line.endswith(",") or line.endswith("\\") or line.endswith("("): in_continuation = True head_indent = indent break @@ -513,7 +526,7 @@ def insert_output_start_stop_indicators(src): input_block_number += 1 break - return '\n'.join(newlines) + return "\n".join(newlines) def consolidate_input_blocks(input_blocks, output_blocks): @@ -524,22 +537,22 @@ def consolidate_input_blocks(input_blocks, output_blocks): Remove any leading and trailing blank lines from all input blocks. """ new_input_blocks = [] - new_block = '' + new_block = "" for (code, tag) in input_blocks: if tag not in output_blocks: # no output, add to new consolidated block - if new_block and not new_block.endswith('\n'): - new_block += '\n' + if new_block and not new_block.endswith("\n"): + new_block += "\n" new_block += code elif new_block: # add current input to new consolidated block and save - if new_block and not new_block.endswith('\n'): - new_block += '\n' + if new_block and not new_block.endswith("\n"): + new_block += "\n" new_block += code new_block = remove_leading_trailing_whitespace_lines(new_block) new_input_blocks.append(InputBlock(new_block, tag)) - new_block = '' + new_block = "" else: # just strip leading/trailing from input block code = remove_leading_trailing_whitespace_lines(code) @@ -548,7 +561,7 @@ def consolidate_input_blocks(input_blocks, output_blocks): # trailing input with no corresponding output if new_block: new_block = remove_leading_trailing_whitespace_lines(new_block) - new_input_blocks.append(InputBlock(new_block, '')) + new_input_blocks.append(InputBlock(new_block, "")) return new_input_blocks @@ -576,8 +589,8 @@ def extract_output_blocks(run_output): for line in run_output.splitlines(): if output_block is None: output_block = [] - if line[:5] == '>>>>>': - output = ('\n'.join(output_block)).strip() + if line[:5] == ">>>>>": + output = ("\n".join(output_block)).strip() if output: output_blocks[line] = output output_block = None @@ -587,7 +600,7 @@ def extract_output_blocks(run_output): if output_block is not None: # It is possible to have trailing output # (e.g. if the last print_producing statement is in a try block) - output_blocks['Trailing'] = output_block + output_blocks["Trailing"] = output_block return output_blocks @@ -606,6 +619,7 @@ def strip_decorators(src): str Source code minus any decorators """ + class Parser(ast.NodeVisitor): def __init__(self): self.function_node = None @@ -636,7 +650,7 @@ def get_function(self): raise RuntimeError("Cannot determine line number for decorated function without args") lines = src.splitlines() - undecorated_src = '\n'.join(lines[function_lineno - 1:]) + undecorated_src = "\n".join(lines[function_lineno - 1 :]) return undecorated_src @@ -653,7 +667,7 @@ def strip_header(src): src : str source code """ - lines = src.split('\n') + lines = src.split("\n") first_len = None for i, line in enumerate(lines): n1 = len(line) @@ -664,9 +678,9 @@ def strip_header(src): elif n1 == 0: continue if tab != first_len: - return '\n'.join(lines[i:]) + return "\n".join(lines[i:]) - return '' + return "" def dedent(src): @@ -679,14 +693,14 @@ def dedent(src): source code """ - lines = src.split('\n') + lines = src.split("\n") if lines: for i, line in enumerate(lines): lstrip = line.lstrip() - if lstrip: # keep going if first line(s) are blank. + if lstrip: # keep going if first line(s) are blank. tab = len(line) - len(lstrip) - return '\n'.join(l[tab:] for l in lines[i:]) - return '' + return "\n".join(l[tab:] for l in lines[i:]) + return "" def sync_multi_output_blocks(run_output): @@ -738,7 +752,7 @@ def run_code(code_to_run, path, module=None, cls=None, shows_plot=False, imports except ImportError: use_mpi = False else: - N_PROCS = getattr(cls, 'N_PROCS', 1) + N_PROCS = getattr(cls, "N_PROCS", 1) use_mpi = N_PROCS > 1 try: @@ -758,31 +772,34 @@ def run_code(code_to_run, path, module=None, cls=None, shows_plot=False, imports env = os.environ.copy() # output will be written to one file per process - env['USE_PROC_FILES'] = '1' + env["USE_PROC_FILES"] = "1" - env['OPENMDAO_CURRENT_MODULE'] = module.__name__ - env['OPENMDAO_CODE_TO_RUN'] = code_to_run + env["OPENMDAO_CURRENT_MODULE"] = module.__name__ + env["OPENMDAO_CODE_TO_RUN"] = code_to_run - p = subprocess.Popen(['mpirun', '-n', str(N_PROCS), sys.executable, _sub_runner], - env=env) + p = subprocess.Popen(["mpirun", "-n", str(N_PROCS), sys.executable, _sub_runner], env=env) p.wait() # extract output blocks from all output files & merge them output = [] for i in range(N_PROCS): - with open('%d.out' % i) as f: + with open("%d.out" % i) as f: output.append(f.read()) - os.remove('%d.out' % i) + os.remove("%d.out" % i) elif shows_plot: if module is None: # write code to a file so we can run it. fd, code_to_run_path = tempfile.mkstemp() - with os.fdopen(fd, 'w') as tmp: + with os.fdopen(fd, "w") as tmp: tmp.write(code_to_run) try: - p = subprocess.Popen([sys.executable, code_to_run_path], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=os.environ) + p = subprocess.Popen( + [sys.executable, code_to_run_path], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=os.environ, + ) output, _ = p.communicate() if p.returncode != 0: failed = True @@ -792,16 +809,17 @@ def run_code(code_to_run, path, module=None, cls=None, shows_plot=False, imports else: env = os.environ.copy() - env['OPENMDAO_CURRENT_MODULE'] = module.__name__ - env['OPENMDAO_CODE_TO_RUN'] = code_to_run + env["OPENMDAO_CURRENT_MODULE"] = module.__name__ + env["OPENMDAO_CODE_TO_RUN"] = code_to_run - p = subprocess.Popen([sys.executable, _sub_runner], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) + p = subprocess.Popen( + [sys.executable, _sub_runner], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env + ) output, _ = p.communicate() if p.returncode != 0: failed = True - output = output.decode('utf-8', 'ignore') + output = output.decode("utf-8", "ignore") else: # just exec() the code for serial tests. @@ -817,10 +835,10 @@ def run_code(code_to_run, path, module=None, cls=None, shows_plot=False, imports if module is None: globals_dict = { - '__file__': path, - '__name__': '__main__', - '__package__': None, - '__cached__': None, + "__file__": path, + "__name__": "__main__", + "__package__": None, + "__cached__": None, } else: if imports_not_required: @@ -835,8 +853,8 @@ def run_code(code_to_run, path, module=None, cls=None, shows_plot=False, imports except Exception as err: # for actual errors, print code (with line numbers) to facilitate debugging if not isinstance(err, unittest.SkipTest): - for n, line in enumerate(code_to_run.split('\n')): - print('%4d: %s' % (n, line), file=stderr) + for n, line in enumerate(code_to_run.split("\n")): + print("%4d: %s" % (n, line), file=stderr) raise finally: sys.stdout = stdout @@ -845,10 +863,10 @@ def run_code(code_to_run, path, module=None, cls=None, shows_plot=False, imports output = strout.getvalue() except subprocess.CalledProcessError as e: - output = e.output.decode('utf-8', 'ignore') + output = e.output.decode("utf-8", "ignore") # Get a traceback. - if 'raise unittest.SkipTest' in output: - reason_for_skip = output.splitlines()[-1][len('unittest.case.SkipTest: '):] + if "raise unittest.SkipTest" in output: + reason_for_skip = output.splitlines()[-1][len("unittest.case.SkipTest: ") :] output = reason_for_skip skipped = True else: @@ -886,22 +904,22 @@ def get_interleaved_io_nodes(input_blocks, output_blocks): for (code, tag) in input_blocks: input_node = nodes.literal_block(code, code) - input_node['language'] = 'python' + input_node["language"] = "python" nodelist.append(input_node) if tag and tag in output_blocks: outp = cgiesc.escape(output_blocks[tag]) - if (outp.strip()): + if outp.strip(): output_node = in_or_out_node(kind="Out", number=n, text=outp) nodelist.append(output_node) n += 1 - if 'Trailing' in output_blocks: - output_node = in_or_out_node(kind="Out", number=n, text=output_blocks['Trailing']) + if "Trailing" in output_blocks: + output_node = in_or_out_node(kind="Out", number=n, text=output_blocks["Trailing"]) nodelist.append(output_node) return nodelist def get_output_block_node(output_blocks): - output_block = '\n'.join([cgiesc.escape(ob) for ob in output_blocks]) + output_block = "\n".join([cgiesc.escape(ob) for ob in output_blocks]) return in_or_out_node(kind="Out", number=1, text=output_block) diff --git a/docs/_exts/embed_bibtex.py b/docs/_exts/embed_bibtex.py index e14c254e..fe3e072a 100644 --- a/docs/_exts/embed_bibtex.py +++ b/docs/_exts/embed_bibtex.py @@ -1,4 +1,3 @@ - import sys import importlib @@ -32,7 +31,9 @@ def depart_bibtex_node(self, node): html = """
{}
-
""".format(node["text"]) + """.format( + node["text"] + ) self.body.append(html) @@ -62,7 +63,7 @@ def run(self): mod = importlib.import_module(module_path) obj = getattr(mod, class_name)() - if not hasattr(obj, 'cite') or not obj.cite: + if not hasattr(obj, "cite") or not obj.cite: raise SphinxError("Couldn't find 'cite' in class '%s'" % class_name) return [bibtex_node(text=obj.cite)] @@ -70,7 +71,7 @@ def run(self): def setup(app): """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive('embed-bibtex', EmbedBibtexDirective) + app.add_directive("embed-bibtex", EmbedBibtexDirective) app.add_node(bibtex_node, html=(visit_bibtex_node, depart_bibtex_node)) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/docs/_exts/embed_code.py b/docs/_exts/embed_code.py index 63a9dae6..80c86c1d 100644 --- a/docs/_exts/embed_code.py +++ b/docs/_exts/embed_code.py @@ -15,17 +15,29 @@ sys.path.insert(0, this_directory) -from docutil_legacy import get_source_code, remove_docstrings, \ - remove_initial_empty_lines, replace_asserts_with_prints, \ - strip_header, dedent, insert_output_start_stop_indicators, run_code, \ - get_skip_output_node, get_interleaved_io_nodes, get_output_block_node, \ - split_source_into_input_blocks, extract_output_blocks, consolidate_input_blocks, node_setup, \ - strip_decorators +from docutil_legacy import ( + get_source_code, + remove_docstrings, + remove_initial_empty_lines, + replace_asserts_with_prints, + strip_header, + dedent, + insert_output_start_stop_indicators, + run_code, + get_skip_output_node, + get_interleaved_io_nodes, + get_output_block_node, + split_source_into_input_blocks, + extract_output_blocks, + consolidate_input_blocks, + node_setup, + strip_decorators, +) _plot_count = 0 -plotting_functions = ['\.show\(', 'partial_deriv_plot\('] +plotting_functions = ["\.show\(", "partial_deriv_plot\("] class EmbedCodeDirective(Directive): @@ -52,11 +64,11 @@ class EmbedCodeDirective(Directive): has_content = True option_spec = { - 'strip-docstrings': unchanged, - 'layout': unchanged, - 'scale': unchanged, - 'align': unchanged, - 'imports-not-required': unchanged, + "strip-docstrings": unchanged, + "layout": unchanged, + "scale": unchanged, + "align": unchanged, + "imports-not-required": unchanged, } def run(self): @@ -65,12 +77,12 @@ def run(self): # # error checking # - allowed_layouts = set(['code', 'output', 'interleave', 'plot']) + allowed_layouts = set(["code", "output", "interleave", "plot"]) - if 'layout' in self.options: - layout = [s.strip() for s in self.options['layout'].split(',')] + if "layout" in self.options: + layout = [s.strip() for s in self.options["layout"].split(",")] else: - layout = ['code'] + layout = ["code"] if len(layout) > len(set(layout)): raise SphinxError("No duplicate layout entries allowed.") @@ -79,9 +91,8 @@ def run(self): if bad: raise SphinxError("The following layout options are invalid: %s" % bad) - if 'interleave' in layout and ('code' in layout or 'output' in layout): - raise SphinxError("The interleave option is mutually exclusive to the code " - "and output options.") + if "interleave" in layout and ("code" in layout or "output" in layout): + raise SphinxError("The interleave option is mutually exclusive to the code " "and output options.") # # Get the source code @@ -99,15 +110,15 @@ def run(self): # # script, test and/or plot? # - is_script = path.endswith('.py') + is_script = path.endswith(".py") is_test = class_ is not None and inspect.isclass(class_) and issubclass(class_, unittest.TestCase) - shows_plot = re.compile('|'.join(plotting_functions)).search(source) + shows_plot = re.compile("|".join(plotting_functions)).search(source) - if 'plot' in layout: + if "plot" in layout: plot_dir = os.getcwd() - plot_fname = 'doc_plot_%d.png' % _plot_count + plot_fname = "doc_plot_%d.png" % _plot_count _plot_count += 1 plot_file_abs = os.path.join(os.path.abspath(plot_dir), plot_fname) @@ -118,13 +129,13 @@ def run(self): # # Modify the source prior to running # - if 'strip-docstrings' in self.options: + if "strip-docstrings" in self.options: source = remove_docstrings(source) - setup_code = '' - teardown_code = '' - mpl_import = '' - mpl_figure = '' + setup_code = "" + teardown_code = "" + mpl_import = "" + mpl_figure = "" if is_test: try: @@ -136,32 +147,42 @@ def run(self): source = remove_initial_empty_lines(source) class_name = class_.__name__ - method_name = path.rsplit('.', 1)[1] + method_name = path.rsplit(".", 1)[1] # make 'self' available to test code (as an instance of the test case) - self_code = "from %s import %s\nself = %s('%s')\n" % \ - (module.__name__, class_name, class_name, method_name) + self_code = "from %s import %s\nself = %s('%s')\n" % ( + module.__name__, + class_name, + class_name, + method_name, + ) # get setUp and tearDown but don't duplicate if it is the method being tested - setup_code = '' if method_name == 'setUp' else dedent(strip_header(remove_docstrings( - inspect.getsource(getattr(class_, 'setUp'))))) - - teardown_code = '' if method_name == 'tearDown' else dedent(strip_header( - remove_docstrings(inspect.getsource(getattr(class_, 'tearDown'))))) + setup_code = ( + "" + if method_name == "setUp" + else dedent(strip_header(remove_docstrings(inspect.getsource(getattr(class_, "setUp"))))) + ) + + teardown_code = ( + "" + if method_name == "tearDown" + else dedent(strip_header(remove_docstrings(inspect.getsource(getattr(class_, "tearDown"))))) + ) # for interleaving, we need to mark input/output blocks - if 'interleave' in layout: + if "interleave" in layout: interleaved = insert_output_start_stop_indicators(source) - code_to_run = '\n'.join([self_code, setup_code, interleaved, teardown_code]).strip() + code_to_run = "\n".join([self_code, setup_code, interleaved, teardown_code]).strip() else: - code_to_run = '\n'.join([self_code, setup_code, source, teardown_code]).strip() + code_to_run = "\n".join([self_code, setup_code, source, teardown_code]).strip() except Exception: err = traceback.format_exc() raise SphinxError("Problem with embed of " + path + ": \n" + str(err)) else: if indent > 0: source = dedent(source) - if 'interleave' in layout: + if "interleave" in layout: source = insert_output_start_stop_indicators(source) code_to_run = source[:] @@ -170,34 +191,41 @@ def run(self): # skipped = failed = False - if 'output' in layout or 'interleave' in layout or 'plot' in layout: + if "output" in layout or "interleave" in layout or "plot" in layout: - imports_not_required = 'imports-not-required' in self.options + imports_not_required = "imports-not-required" in self.options if shows_plot: # NOTE: import matplotlib AFTER __future__ (if it's there) # All use of __future__ has been removed from OpenMDAO with v3.x # so the related code has been removed here as well. - mpl_import = "\n".join([ - "import warnings", - "import matplotlib", - "warnings.filterwarnings('ignore')", - "matplotlib.use('Agg')\n" - ]) + mpl_import = "\n".join( + [ + "import warnings", + "import matplotlib", + "warnings.filterwarnings('ignore')", + "matplotlib.use('Agg')\n", + ] + ) code_to_run = mpl_import + code_to_run - if 'plot' in layout: + if "plot" in layout: mpl_figure = '\nmatplotlib.pyplot.savefig("%s")' % plot_file_abs code_to_run = code_to_run + mpl_figure - if is_test and getattr(method, '__unittest_skip__', False): + if is_test and getattr(method, "__unittest_skip__", False): skipped = True failed = False run_outputs = method.__unittest_skip_why__ else: - skipped, failed, run_outputs = run_code(code_to_run, path, module=module, cls=class_, - imports_not_required=imports_not_required, - shows_plot=shows_plot) + skipped, failed, run_outputs = run_code( + code_to_run, + path, + module=module, + cls=class_, + imports_not_required=imports_not_required, + shows_plot=shows_plot, + ) # # Handle output @@ -219,10 +247,10 @@ def run(self): io_nodes = [get_skip_output_node(run_outputs)] else: - if 'output' in layout: + if "output" in layout: output_blocks = run_outputs if isinstance(run_outputs, list) else [run_outputs] - elif 'interleave' in layout: + elif "interleave" in layout: if is_test: start = len(self_code) + len(setup_code) + len(mpl_import) end = len(code_to_run) - len(teardown_code) - len(mpl_figure) @@ -236,20 +264,27 @@ def run(self): # with subsequent input blocks that do have output input_blocks = consolidate_input_blocks(input_blocks, output_blocks) - if 'plot' in layout: + if "plot" in layout: if not os.path.isfile(plot_file_abs): raise SphinxError("Can't find plot file '%s'" % plot_file_abs) - directive_dir = os.path.relpath(os.getcwd(), - os.path.dirname(self.state.document.settings._source)) + directive_dir = os.path.relpath(os.getcwd(), os.path.dirname(self.state.document.settings._source)) # this filename must NOT contain an absolute path, else the Figure will not # be able to find the image file in the generated html dir. plot_file = os.path.join(directive_dir, plot_fname) # create plot node - fig = images.Figure(self.name, [plot_file], self.options, self.content, self.lineno, - self.content_offset, self.block_text, self.state, - self.state_machine) + fig = images.Figure( + self.name, + [plot_file], + self.options, + self.content, + self.lineno, + self.content_offset, + self.block_text, + self.state, + self.state_machine, + ) plot_nodes = fig.run() # @@ -258,22 +293,22 @@ def run(self): doc_nodes = [] skip_fail_shown = False for opt in layout: - if opt == 'code': + if opt == "code": # we want the body of code to be formatted and code highlighted body = nodes.literal_block(source, source) - body['language'] = 'python' + body["language"] = "python" doc_nodes.append(body) elif skipped: if not skip_fail_shown: body = nodes.literal_block(source, source) - body['language'] = 'python' + body["language"] = "python" doc_nodes.append(body) doc_nodes.extend(io_nodes) skip_fail_shown = True else: - if opt == 'interleave': + if opt == "interleave": doc_nodes.extend(get_interleaved_io_nodes(input_blocks, output_blocks)) - elif opt == 'output': + elif opt == "output": doc_nodes.append(get_output_block_node(output_blocks)) else: # plot doc_nodes.extend(plot_nodes) @@ -283,7 +318,7 @@ def run(self): def setup(app): """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive('embed-code', EmbedCodeDirective) + app.add_directive("embed-code", EmbedCodeDirective) node_setup(app) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/docs/_exts/embed_compare.py b/docs/_exts/embed_compare.py index da0fdbda..ecd9714a 100644 --- a/docs/_exts/embed_compare.py +++ b/docs/_exts/embed_compare.py @@ -25,12 +25,12 @@ class ContentContainerDirective(Directive): def run(self): self.assert_has_content() - text = '\n'.join(self.content) + text = "\n".join(self.content) node = nodes.container(text) - node['classes'].append('rosetta_outer') + node["classes"].append("rosetta_outer") if self.arguments and self.arguments[0]: - node['classes'].append(self.arguments[0]) + node["classes"].append(self.arguments[0]) self.add_name(node) self.state.nested_parse(self.content, self.content_offset, node) @@ -74,21 +74,21 @@ def run(self): doc_nodes = [] # Choose style - left_style = 'rosetta_left' - right_style = 'rosetta_right' + left_style = "rosetta_left" + right_style = "rosetta_right" if len(arg) == 4: - if arg[3] == 'style2': - left_style = 'rosetta_left2' - right_style = 'rosetta_right2' - elif arg[3] == 'no_compare': + if arg[3] == "style2": + left_style = "rosetta_left2" + right_style = "rosetta_right2" + elif arg[3] == "no_compare": compare = False # LEFT side = Old OpenMDAO if compare: - text = '\n'.join(self.content) + text = "\n".join(self.content) left_body = nodes.literal_block(text, text) - left_body['language'] = 'python' - left_body['classes'].append(left_style) + left_body["language"] = "python" + left_body["classes"].append(left_style) # for RIGHT side, get the code block, and reduce it if requested right_method = arg[0] @@ -96,7 +96,7 @@ def run(self): if len(arg) >= 3: start_txt = arg[1] end_txt = arg[2] - lines = text.split('\n') + lines = text.split("\n") istart = 0 for j, line in enumerate(lines): @@ -108,7 +108,7 @@ def run(self): iend = len(lines) for j, line in enumerate(lines): if end_txt in line: - iend = j+1 + iend = j + 1 break lines = lines[:iend] @@ -116,20 +116,20 @@ def run(self): # Remove the check suppression. for j, line in enumerate(lines): if "prob.setup(check=False" in line: - lines[j] = lines[j].replace('check=False, ', '') - lines[j] = lines[j].replace('check=False', '') + lines[j] = lines[j].replace("check=False, ", "") + lines[j] = lines[j].replace("check=False", "") # prune whitespace down to match first line - while lines[0].startswith(' '): + while lines[0].startswith(" "): lines = [line[4:] for line in lines] - text = '\n'.join(lines) + text = "\n".join(lines) # RIGHT side = Current OpenMDAO right_body = nodes.literal_block(text, text) - right_body['language'] = 'python' + right_body["language"] = "python" if compare: - right_body['classes'].append(right_style) + right_body["classes"].append(right_style) if compare: doc_nodes.append(left_body) @@ -140,7 +140,7 @@ def run(self): def setup(app): """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive('content-container', ContentContainerDirective) - app.add_directive('embed-compare', EmbedCompareDirective) + app.add_directive("content-container", ContentContainerDirective) + app.add_directive("embed-compare", EmbedCompareDirective) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/docs/_exts/embed_n2.py b/docs/_exts/embed_n2.py index 45fbab97..55184f8c 100644 --- a/docs/_exts/embed_n2.py +++ b/docs/_exts/embed_n2.py @@ -37,7 +37,7 @@ class EmbedN2Directive(Directive): def run(self): path_to_model = self.arguments[0] - n2_dims = [ 1200, 700 ] + n2_dims = [1200, 700] show_toolbar = False if len(self.arguments) > 1 and self.arguments[1]: @@ -53,23 +53,21 @@ def run(self): # check that the file exists if not os.path.isfile(np): - raise IOError('File does not exist({0})'.format(np)) - + raise IOError("File does not exist({0})".format(np)) + # Generate N2 files into the target_dir. Those files are later copied # into the top of the HTML hierarchy, so the HTML doc file needs a # relative path to them. target_dir = os.path.join(os.getcwd(), "_n2html") - rel_dir = os.path.relpath(os.getcwd(), - os.path.dirname(self.state.document.settings._source)) - html_base_name = os.path.basename(path_to_model).split('.')[0] + "_n2.html" + rel_dir = os.path.relpath(os.getcwd(), os.path.dirname(self.state.document.settings._source)) + html_base_name = os.path.basename(path_to_model).split(".")[0] + "_n2.html" html_name = os.path.join(target_dir, html_base_name) html_rel_name = os.path.join(rel_dir, html_base_name) if show_toolbar: - html_rel_name += '#toolbar' + html_rel_name += "#toolbar" - cmd = subprocess.Popen( - ['openmdao', 'n2', np, '--no_browser', '--embed', '-o' + html_name]) + cmd = subprocess.Popen(["openmdao", "n2", np, "--no_browser", "--embed", "-o" + html_name]) cmd_out, cmd_err = cmd.communicate() rst = ViewList() @@ -103,6 +101,6 @@ def run(self): def setup(app): """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive('embed-n2', EmbedN2Directive) + app.add_directive("embed-n2", EmbedN2Directive) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/docs/_exts/embed_options.py b/docs/_exts/embed_options.py index be71a97a..bc0f5a47 100644 --- a/docs/_exts/embed_options.py +++ b/docs/_exts/embed_options.py @@ -1,4 +1,3 @@ - import importlib from docutils import nodes @@ -49,8 +48,8 @@ def run(self): n += 1 # Note applicable to System, Solver and Driver 'options', but not to 'recording_options' - if attribute_name != 'recording_options': - lines.append("", "options table", n+1) # Blank line required after table. + if attribute_name != "recording_options": + lines.append("", "options table", n + 1) # Blank line required after table. # Create a node. node = nodes.section() @@ -65,6 +64,6 @@ def run(self): def setup(app): """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive('embed-options', EmbedOptionsDirective) + app.add_directive("embed-options", EmbedOptionsDirective) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/docs/_exts/embed_shell_cmd.py b/docs/_exts/embed_shell_cmd.py index 5f281b35..0717bb28 100644 --- a/docs/_exts/embed_shell_cmd.py +++ b/docs/_exts/embed_shell_cmd.py @@ -40,7 +40,9 @@ def depart_failed_node(self, node):
{}
- """.format(node["text"]) + """.format( + node["text"] + ) self.body.append(html) @@ -64,7 +66,9 @@ def depart_cmd_node(self, node): html = """
{}
-
""".format(node["text"]) + """.format( + node["text"] + ) self.body.append(html) @@ -89,30 +93,30 @@ class EmbedShellCmdDirective(Directive): has_content = False option_spec = { - 'cmd': unchanged, # shell command to execute - 'dir': unchanged, # working dir - 'show_cmd': unchanged, # set this to make the shell command visible - 'stderr': unchanged # set this to include stderr contents with the output + "cmd": unchanged, # shell command to execute + "dir": unchanged, # working dir + "show_cmd": unchanged, # set this to make the shell command visible + "stderr": unchanged, # set this to include stderr contents with the output } def run(self): """ Create a list of document nodes to return. """ - if 'cmd' in self.options: - cmdstr = self.options['cmd'] + if "cmd" in self.options: + cmdstr = self.options["cmd"] cmd = cmdstr.split() else: raise SphinxError("'cmd' is not defined for embed-shell-cmd.") startdir = os.getcwd() - if 'dir' in self.options: - workdir = os.path.abspath(os.path.expandvars(os.path.expanduser(self.options['dir']))) + if "dir" in self.options: + workdir = os.path.abspath(os.path.expandvars(os.path.expanduser(self.options["dir"]))) else: workdir = os.getcwd() - if 'stderr' in self.options: + if "stderr" in self.options: stderr = subprocess.STDOUT else: stderr = None @@ -120,17 +124,17 @@ def run(self): os.chdir(workdir) try: - output = subprocess.check_output(cmd, stderr=subprocess.STDOUT,env=os.environ).decode('utf-8', 'ignore') + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, env=os.environ).decode("utf-8", "ignore") except subprocess.CalledProcessError as err: # Failed cases raised as a Directive warning (level 2 in docutils). # This way, the sphinx build does not terminate if, for example, you are building on # an environment where mpi or pyoptsparse are missing. - msg = "Running of embedded shell command '{}' in docs failed. Output was: \n{}" \ - .format(cmdstr, err.output.decode('utf-8')) + msg = "Running of embedded shell command '{}' in docs failed. Output was: \n{}".format( + cmdstr, err.output.decode("utf-8") + ) raise self.directive_error(2, msg) except Exception as err: - msg = "Running of embedded shell command '{}' in docs failed. Output was: \n{}" \ - .format(cmdstr, err) + msg = "Running of embedded shell command '{}' in docs failed. Output was: \n{}".format(cmdstr, err) raise self.directive_error(2, msg) finally: os.chdir(startdir) @@ -138,12 +142,12 @@ def run(self): output = cgiesc.escape(output) show = True - if 'show_cmd' in self.options: - show = self.options['show_cmd'].lower().strip() == 'true' + if "show_cmd" in self.options: + show = self.options["show_cmd"].lower().strip() == "true" if show: input_node = nodes.literal_block(cmdstr, cmdstr) - input_node['language'] = 'none' + input_node["language"] = "none" output_node = cmd_node(text=output) @@ -155,8 +159,8 @@ def run(self): def setup(app): """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive('embed-shell-cmd', EmbedShellCmdDirective) + app.add_directive("embed-shell-cmd", EmbedShellCmdDirective) app.add_node(failed_node, html=(visit_failed_node, depart_failed_node)) app.add_node(cmd_node, html=(visit_cmd_node, depart_cmd_node)) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/docs/_exts/link_class_from_docstring.py b/docs/_exts/link_class_from_docstring.py index ca251904..8b1c2684 100644 --- a/docs/_exts/link_class_from_docstring.py +++ b/docs/_exts/link_class_from_docstring.py @@ -12,13 +12,14 @@ om_classes = {} + def build_dict(): global om_classes - for importer, modname, ispkg in pkgutil.walk_packages(path=package.__path__, - prefix=package.__name__ + '.', - onerror=lambda x: None): + for importer, modname, ispkg in pkgutil.walk_packages( + path=package.__path__, prefix=package.__name__ + ".", onerror=lambda x: None + ): if not ispkg: - if 'docs' not in modname: + if "docs" not in modname: if any(ignore in modname for ignore in IGNORE_LIST): continue module = importer.find_module(modname).load_module(modname) @@ -37,7 +38,7 @@ def om_process_docstring(app, what, name, obj, options, lines): for i in range(len(lines)): # create a regex pattern to match - pat = r'(<.*?>)' + pat = r"(<.*?>)" # find all matches of the pattern in a line match = re.findall(pat, lines[i]) if match: @@ -49,11 +50,11 @@ def om_process_docstring(app, what, name, obj, options, lines): continue # if there's a dot in the pattern, it's a method # e.g. - if '.' in m: + if "." in m: # need to grab the class name and method name separately - split_match = m.split('.') + split_match = m.split(".") justclass = split_match[0] # class - justmeth = split_match[1] # method + justmeth = split_match[1] # method if justclass in om_classes: classfullpath = om_classes[justclass] # construct a link :meth:`class.method ` @@ -62,8 +63,7 @@ def om_process_docstring(app, what, name, obj, options, lines): lines[i] = lines[i].replace(ma, link) else: # the class isn't in the class table! - print("WARNING: {} not found in dictionary of OpenMDAO methods".format - (justclass)) + print("WARNING: {} not found in dictionary of OpenMDAO methods".format(justclass)) # replace instances of with just class in docstring # (strip angle brackets) lines[i] = lines[i].replace(ma, m) @@ -74,8 +74,7 @@ def om_process_docstring(app, what, name, obj, options, lines): lines[i] = lines[i].replace(ma, ":class:`~" + classfullpath + "`") else: # the class isn't in the class table! - print("WARNING: {} not found in dictionary of OpenMDAO classes" - .format(m)) + print("WARNING: {} not found in dictionary of OpenMDAO classes".format(m)) # replace instances of with class in docstring # (strip angle brackets) lines[i] = lines[i].replace(ma, m) @@ -84,6 +83,5 @@ def om_process_docstring(app, what, name, obj, options, lines): # This is the crux of the extension--connecting an internal # Sphinx event, "autodoc-process-docstring" with our own custom function. def setup(app): - """ - """ - app.connect('autodoc-process-docstring', om_process_docstring) + """ """ + app.connect("autodoc-process-docstring", om_process_docstring) diff --git a/docs/_exts/tags.py b/docs/_exts/tags.py index 704baf2a..75d2a5af 100644 --- a/docs/_exts/tags.py +++ b/docs/_exts/tags.py @@ -13,12 +13,12 @@ def setup(app): # This adds a new node class to build sys, with custom functs, (same name as file) app.add_node(tag, html=(visit_tag_node, depart_tag_node)) # This creates a new ".. tags:: " directive in Sphinx - app.add_directive('tags', TagDirective) + app.add_directive("tags", TagDirective) # These are event handlers, functions connected to events. - app.connect('doctree-resolved', process_tag_nodes) - app.connect('env-purge-doc', purge_tags) + app.connect("doctree-resolved", process_tag_nodes) + app.connect("env-purge-doc", purge_tags) # Identifies the version of our extension - return {'version': '0.1'} + return {"version": "0.1"} def visit_tag_node(self, node): @@ -37,7 +37,7 @@ def process_tag_nodes(app, doctree, fromdocname): env = app.builder.env -class tag (nodes.Admonition, nodes.Element): +class tag(nodes.Admonition, nodes.Element): pass @@ -47,8 +47,8 @@ class TagDirective(Directive): def run(self): env = self.state.document.settings.env - targetid = "tag-%d" % env.new_serialno('tag') - targetnode = nodes.target('', '', ids=[targetid]) + targetid = "tag-%d" % env.new_serialno("tag") + targetnode = nodes.target("", "", ids=[targetid]) # The tags fetched from the custom directive are one piece of text # sitting in self.content[0] @@ -65,8 +65,16 @@ def run(self): # Replace content[0] with hyperlinks to display in admonition self.content[0] = linkjoin - ad = Admonition(self.name, [_('Tags')], self.options, - self.content, self.lineno, self.content_offset, - self.block_text, self.state, self.state_machine) + ad = Admonition( + self.name, + [_("Tags")], + self.options, + self.content, + self.lineno, + self.content_offset, + self.block_text, + self.state, + self.state_machine, + ) return [targetnode] + ad.run() diff --git a/docs/conf.py b/docs/conf.py index 388c7e02..c140de31 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,9 +19,9 @@ from sphinx_mdolab_theme.config import * -sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('..')) -sys.path.insert(0, os.path.abspath('./_exts')) +sys.path.insert(0, os.path.abspath(".")) +sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, os.path.abspath("./_exts")) # sphinx build needs to be able to find the openmdao embed_code plugin # so we add it to the path this_directory = os.path.abspath(os.path.dirname(__file__)) @@ -67,6 +67,7 @@ def generate_src_docs(dir, top, packages): doc_dir = os.path.join(docs_dir, "_srcdocs") if os.path.isdir(doc_dir): import shutil + shutil.rmtree(doc_dir) if not os.path.isdir(doc_dir): @@ -94,17 +95,17 @@ def generate_src_docs(dir, top, packages): # a package is e.g. openmdao.core, that contains source files # a sub_package, is a src file, e.g. openmdao.core.component sub_packages = [] - package_filename = os.path.join(packages_dir, - "openconcept." + package + ".rst") + package_filename = os.path.join(packages_dir, "openconcept." + package + ".rst") package_name = "openconcept." + package # the sub_listing is going into each package dir and listing what's in it - for sub_listing in sorted(os.listdir(os.path.join(top, package.replace('.','/')))): + for sub_listing in sorted(os.listdir(os.path.join(top, package.replace(".", "/")))): # don't want to catalog files twice, nor use init files nor test dir - if (os.path.isdir(sub_listing) and sub_listing != "tests") or \ - (sub_listing.endswith(".py") and not sub_listing.startswith('_')): + if (os.path.isdir(sub_listing) and sub_listing != "tests") or ( + sub_listing.endswith(".py") and not sub_listing.startswith("_") + ): # just want the name of e.g. dataxfer not dataxfer.py - sub_packages.append(sub_listing.rsplit('.')[0]) + sub_packages.append(sub_listing.rsplit(".")[0]) if len(sub_packages) > 0: # continue to write in the top-level index file. @@ -126,7 +127,7 @@ def generate_src_docs(dir, top, packages): package_file.write(package_top) for sub_package in sub_packages: - SKIP_SUBPACKAGES = ['__pycache__'] + SKIP_SUBPACKAGES = ["__pycache__"] # this line writes subpackage name e.g. "core/component.py" # into the corresponding package index file (e.g. "openmdao.core.rst") if sub_package not in SKIP_SUBPACKAGES: @@ -141,8 +142,7 @@ def generate_src_docs(dir, top, packages): # get the meat of the ref sheet code done filename = sub_package + ".py" ref_sheet.write(".. index:: " + filename + "\n\n") - ref_sheet.write(".. _" + package_name + "." + - filename + ":\n\n") + ref_sheet.write(".. _" + package_name + "." + filename + ":\n\n") ref_sheet.write(filename + "\n") ref_sheet.write("-" * len(filename) + "\n\n") ref_sheet.write(".. automodule:: " + package_name + "." + sub_package) @@ -199,104 +199,116 @@ def run_file_move_result(file_name, output_files, destination_files, optional_cl from sphinx.ext.napoleon.docstring import NumpyDocstring + def parse_inputs_section(self, section): - return self._format_fields('Inputs', self._consume_fields()) + return self._format_fields("Inputs", self._consume_fields()) + + NumpyDocstring._parse_inputs_section = parse_inputs_section + def parse_options_section(self, section): - return self._format_fields('Options', self._consume_fields()) + return self._format_fields("Options", self._consume_fields()) + + NumpyDocstring._parse_options_section = parse_options_section + def parse_outputs_section(self, section): - return self._format_fields('Outputs', self._consume_fields()) + return self._format_fields("Outputs", self._consume_fields()) + + NumpyDocstring._parse_outputs_section = parse_outputs_section + def patched_parse(self): - self._sections['inputs'] = self._parse_inputs_section - self._sections['outputs'] = self._parse_outputs_section - self._sections['options'] = self._parse_options_section - self._unpatched_parse() + self._sections["inputs"] = self._parse_inputs_section + self._sections["outputs"] = self._parse_outputs_section + self._sections["options"] = self._parse_options_section + self._unpatched_parse() + NumpyDocstring._unpatched_parse = NumpyDocstring._parse NumpyDocstring._parse = patched_parse # -- Project information ----------------------------------------------------- -project = 'OpenConcept' -author = 'Benjamin J. Brelje and Eytan J. Adler' +project = "OpenConcept" +author = "Benjamin J. Brelje and Eytan J. Adler" import openconcept + # The short X.Y version version = openconcept.__version__ # The full version, including alpha/beta/rc tags -release = openconcept.__version__ + ' alpha' +release = openconcept.__version__ + " alpha" # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # -needs_sphinx = '1.5' +needs_sphinx = "1.5" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.napoleon', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinxcontrib.bibtex', - 'sphinx_copybutton', - 'sphinx_mdolab_theme.ext.embed_code', - 'sphinx_mdolab_theme.ext.embed_compare', - 'sphinx_mdolab_theme.ext.embed_n2', + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinxcontrib.bibtex", + "sphinx_copybutton", + "sphinx_mdolab_theme.ext.embed_code", + "sphinx_mdolab_theme.ext.embed_compare", + "sphinx_mdolab_theme.ext.embed_n2", ] autodoc_inherit_docstrings = False -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" autoclass_content = "class" autosummary_generate = [] -# Ignore docs errors +# Ignore docs errors nitpick_ignore_regex = [("py:class", ".*")] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # This sets the bibtex bibliography file(s) to reference in the documentation -bibtex_bibfiles = ['ref.bib'] +bibtex_bibfiles = ["ref.bib"] # -- Options for HTML output ------------------------------------------------- # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'openconceptdoc' +htmlhelp_basename = "openconceptdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -305,15 +317,12 @@ def patched_parse(self): # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -323,8 +332,7 @@ def patched_parse(self): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'openconcept.tex', 'openconcept Documentation', - 'Benjamin J. Brelje', 'manual'), + (master_doc, "openconcept.tex", "openconcept Documentation", "Benjamin J. Brelje", "manual"), ] @@ -332,10 +340,7 @@ def patched_parse(self): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'openconcept', 'openconcept Documentation', - [author], 1) -] +man_pages = [(master_doc, "openconcept", "openconcept Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -344,18 +349,39 @@ def patched_parse(self): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'openconcept', 'openconcept Documentation', - author, 'openconcept', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "openconcept", + "openconcept Documentation", + author, + "openconcept", + "One line description of project.", + "Miscellaneous", + ), ] # -- Extension configuration ------------------------------------------------- # -- Run examples to get figures for docs ------------------------------------ -run_file_move_result("../openconcept/examples/minimal.py", ["minimal_example_results.svg"], ["tutorials/assets/minimal_example_results.svg"], optional_cl_args=["--hide_visuals"]) -run_file_move_result("../openconcept/examples/minimal_integrator.py", ["minimal_integrator_results.svg"], ["tutorials/assets/minimal_integrator_results.svg"], optional_cl_args=["--hide_visuals"]) -run_file_move_result("../openconcept/examples/TBM850.py", ["turboprop_takeoff_results.svg", "turboprop_mission_results.svg"], ["tutorials/assets/turboprop_takeoff_results.svg", "tutorials/assets/turboprop_mission_results.svg"], optional_cl_args=["--hide_visuals"]) +run_file_move_result( + "../openconcept/examples/minimal.py", + ["minimal_example_results.svg"], + ["tutorials/assets/minimal_example_results.svg"], + optional_cl_args=["--hide_visuals"], +) +run_file_move_result( + "../openconcept/examples/minimal_integrator.py", + ["minimal_integrator_results.svg"], + ["tutorials/assets/minimal_integrator_results.svg"], + optional_cl_args=["--hide_visuals"], +) +run_file_move_result( + "../openconcept/examples/TBM850.py", + ["turboprop_takeoff_results.svg", "turboprop_mission_results.svg"], + ["tutorials/assets/turboprop_takeoff_results.svg", "tutorials/assets/turboprop_mission_results.svg"], + optional_cl_args=["--hide_visuals"], +) # Remove the N2 diagrams it also created files_remove = ["minimal_example_n2.html", "minimal_integrator_n2.html", "turboprop_n2.html"] @@ -365,7 +391,7 @@ def patched_parse(self): # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -#intersphinx_mapping = {'https://docs.python.org/': None} +# intersphinx_mapping = {'https://docs.python.org/': None} # -- Options for todo extension ---------------------------------------------- @@ -379,15 +405,15 @@ def patched_parse(self): # os.rename('_srcdocs_native/modules.rst','_srcdocs_native/index.rst') # openmdao way packages = [ - 'aerodynamics', - 'aerodynamics.openaerostruct', - 'atmospherics', - 'energy_storage', - 'mission', - 'propulsion', - 'propulsion.systems', - 'thermal', - 'utilities', - 'utilities.math' + "aerodynamics", + "aerodynamics.openaerostruct", + "atmospherics", + "energy_storage", + "mission", + "propulsion", + "propulsion.systems", + "thermal", + "utilities", + "utilities.math", ] generate_src_docs(".", "../openconcept", packages) diff --git a/openconcept/__init__.py b/openconcept/__init__.py index a9873473..df124332 100644 --- a/openconcept/__init__.py +++ b/openconcept/__init__.py @@ -1 +1 @@ -__version__ = '0.4.2' +__version__ = "0.4.2" diff --git a/openconcept/aerodynamics/aerodynamics.py b/openconcept/aerodynamics/aerodynamics.py index 0b62b9c2..250174d1 100644 --- a/openconcept/aerodynamics/aerodynamics.py +++ b/openconcept/aerodynamics/aerodynamics.py @@ -35,46 +35,60 @@ class PolarDrag(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc="Number of nodes to compute") + self.options.declare("num_nodes", default=1, desc="Number of nodes to compute") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] arange = np.arange(0, nn) - self.add_input('fltcond|CL', shape=(nn,)) - self.add_input('fltcond|q', units='N * m**-2', shape=(nn,)) - self.add_input('ac|geom|wing|S_ref', units='m **2') - self.add_input('CD0') - self.add_input('e') - self.add_input('ac|geom|wing|AR') - self.add_output('drag', units='N', shape=(nn,)) - - self.declare_partials(['drag'], ['fltcond|CL', 'fltcond|q'], rows=arange, cols=arange) - self.declare_partials(['drag'], - ['ac|geom|wing|S_ref', 'ac|geom|wing|AR', 'CD0', 'e'], - rows=arange, cols=np.zeros(nn)) + self.add_input("fltcond|CL", shape=(nn,)) + self.add_input("fltcond|q", units="N * m**-2", shape=(nn,)) + self.add_input("ac|geom|wing|S_ref", units="m **2") + self.add_input("CD0") + self.add_input("e") + self.add_input("ac|geom|wing|AR") + self.add_output("drag", units="N", shape=(nn,)) + + self.declare_partials(["drag"], ["fltcond|CL", "fltcond|q"], rows=arange, cols=arange) + self.declare_partials( + ["drag"], ["ac|geom|wing|S_ref", "ac|geom|wing|AR", "CD0", "e"], rows=arange, cols=np.zeros(nn) + ) def compute(self, inputs, outputs): - outputs['drag'] = (inputs['fltcond|q'] * inputs['ac|geom|wing|S_ref'] * - (inputs['CD0'] + inputs['fltcond|CL']**2 / np.pi / inputs['e'] / - inputs['ac|geom|wing|AR'])) + outputs["drag"] = ( + inputs["fltcond|q"] + * inputs["ac|geom|wing|S_ref"] + * (inputs["CD0"] + inputs["fltcond|CL"] ** 2 / np.pi / inputs["e"] / inputs["ac|geom|wing|AR"]) + ) def compute_partials(self, inputs, J): - J['drag', 'fltcond|q'] = (inputs['ac|geom|wing|S_ref'] * - (inputs['CD0'] + inputs['fltcond|CL']**2 / np.pi / - inputs['e'] / inputs['ac|geom|wing|AR'])) - J['drag', 'fltcond|CL'] = (inputs['fltcond|q'] * inputs['ac|geom|wing|S_ref'] * - (2 * inputs['fltcond|CL'] / np.pi / inputs['e'] / - inputs['ac|geom|wing|AR'])) - J['drag', 'CD0'] = inputs['fltcond|q'] * inputs['ac|geom|wing|S_ref'] - J['drag', 'e'] = - (inputs['fltcond|q'] * inputs['ac|geom|wing|S_ref'] * - inputs['fltcond|CL']**2 / np.pi / - inputs['e']**2 / inputs['ac|geom|wing|AR']) - J['drag', 'ac|geom|wing|S_ref'] = (inputs['fltcond|q'] * (inputs['CD0'] + - inputs['fltcond|CL']**2 / np.pi / inputs['e'] / - inputs['ac|geom|wing|AR'])) - J['drag', 'ac|geom|wing|AR'] = - (inputs['fltcond|q'] * inputs['ac|geom|wing|S_ref'] * - inputs['fltcond|CL']**2 / np.pi / inputs['e'] / - inputs['ac|geom|wing|AR']**2) + J["drag", "fltcond|q"] = inputs["ac|geom|wing|S_ref"] * ( + inputs["CD0"] + inputs["fltcond|CL"] ** 2 / np.pi / inputs["e"] / inputs["ac|geom|wing|AR"] + ) + J["drag", "fltcond|CL"] = ( + inputs["fltcond|q"] + * inputs["ac|geom|wing|S_ref"] + * (2 * inputs["fltcond|CL"] / np.pi / inputs["e"] / inputs["ac|geom|wing|AR"]) + ) + J["drag", "CD0"] = inputs["fltcond|q"] * inputs["ac|geom|wing|S_ref"] + J["drag", "e"] = -( + inputs["fltcond|q"] + * inputs["ac|geom|wing|S_ref"] + * inputs["fltcond|CL"] ** 2 + / np.pi + / inputs["e"] ** 2 + / inputs["ac|geom|wing|AR"] + ) + J["drag", "ac|geom|wing|S_ref"] = inputs["fltcond|q"] * ( + inputs["CD0"] + inputs["fltcond|CL"] ** 2 / np.pi / inputs["e"] / inputs["ac|geom|wing|AR"] + ) + J["drag", "ac|geom|wing|AR"] = -( + inputs["fltcond|q"] + * inputs["ac|geom|wing|S_ref"] + * inputs["fltcond|CL"] ** 2 + / np.pi + / inputs["e"] + / inputs["ac|geom|wing|AR"] ** 2 + ) class Lift(ExplicitComponent): @@ -102,26 +116,26 @@ class Lift(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc="Number of nodes to compute") + self.options.declare("num_nodes", default=1, desc="Number of nodes to compute") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] arange = np.arange(0, nn) - self.add_input('fltcond|CL', shape=(nn,)) - self.add_input('fltcond|q', units='N * m**-2', shape=(nn,)) - self.add_input('ac|geom|wing|S_ref', units='m **2') + self.add_input("fltcond|CL", shape=(nn,)) + self.add_input("fltcond|q", units="N * m**-2", shape=(nn,)) + self.add_input("ac|geom|wing|S_ref", units="m **2") - self.add_output('lift', units='N', shape=(nn,)) - self.declare_partials(['lift'], ['fltcond|CL', 'fltcond|q'], rows=arange, cols=arange) - self.declare_partials(['lift'], ['ac|geom|wing|S_ref'], rows=arange, cols=np.zeros(nn)) + self.add_output("lift", units="N", shape=(nn,)) + self.declare_partials(["lift"], ["fltcond|CL", "fltcond|q"], rows=arange, cols=arange) + self.declare_partials(["lift"], ["ac|geom|wing|S_ref"], rows=arange, cols=np.zeros(nn)) def compute(self, inputs, outputs): - outputs['lift'] = inputs['fltcond|q'] * inputs['ac|geom|wing|S_ref'] * inputs['fltcond|CL'] + outputs["lift"] = inputs["fltcond|q"] * inputs["ac|geom|wing|S_ref"] * inputs["fltcond|CL"] def compute_partials(self, inputs, J): - J['lift', 'fltcond|q'] = inputs['ac|geom|wing|S_ref'] * inputs['fltcond|CL'] - J['lift', 'fltcond|CL'] = inputs['fltcond|q'] * inputs['ac|geom|wing|S_ref'] - J['lift', 'ac|geom|wing|S_ref'] = inputs['fltcond|q'] * inputs['fltcond|CL'] + J["lift", "fltcond|q"] = inputs["ac|geom|wing|S_ref"] * inputs["fltcond|CL"] + J["lift", "fltcond|CL"] = inputs["fltcond|q"] * inputs["ac|geom|wing|S_ref"] + J["lift", "ac|geom|wing|S_ref"] = inputs["fltcond|q"] * inputs["fltcond|CL"] class StallSpeed(ExplicitComponent): @@ -144,30 +158,43 @@ class StallSpeed(ExplicitComponent): """ def setup(self): - self.add_input('weight', units='kg') - self.add_input('ac|geom|wing|S_ref', units='m**2') - self.add_input('CLmax') - self.add_output('Vstall_eas', units='m/s') - self.declare_partials(['Vstall_eas'], ['weight', 'ac|geom|wing|S_ref', 'CLmax']) + self.add_input("weight", units="kg") + self.add_input("ac|geom|wing|S_ref", units="m**2") + self.add_input("CLmax") + self.add_output("Vstall_eas", units="m/s") + self.declare_partials(["Vstall_eas"], ["weight", "ac|geom|wing|S_ref", "CLmax"]) def compute(self, inputs, outputs): rho = 1.225 # kg/m3 - outputs['Vstall_eas'] = np.sqrt(2 * inputs['weight'] * GRAV_CONST / rho / - inputs['ac|geom|wing|S_ref'] / inputs['CLmax']) + outputs["Vstall_eas"] = np.sqrt( + 2 * inputs["weight"] * GRAV_CONST / rho / inputs["ac|geom|wing|S_ref"] / inputs["CLmax"] + ) def compute_partials(self, inputs, J): rho = 1.225 # kg/m3 - J['Vstall_eas', 'weight'] = (1 / np.sqrt(2 * inputs['weight'] * GRAV_CONST / rho / - inputs['ac|geom|wing|S_ref'] / inputs['CLmax']) * - GRAV_CONST / rho / inputs['ac|geom|wing|S_ref'] / inputs['CLmax']) - J['Vstall_eas', 'ac|geom|wing|S_ref'] = - (1 / np.sqrt(2 * inputs['weight'] * GRAV_CONST / rho / - inputs['ac|geom|wing|S_ref'] / - inputs['CLmax']) * - inputs['weight'] * GRAV_CONST / rho / - inputs['ac|geom|wing|S_ref'] ** 2 / - inputs['CLmax']) - J['Vstall_eas', 'CLmax'] = - (1 / np.sqrt(2 * inputs['weight'] * GRAV_CONST / rho / - inputs['ac|geom|wing|S_ref'] / - inputs['CLmax']) * - inputs['weight'] * GRAV_CONST / rho / - inputs['ac|geom|wing|S_ref'] / inputs['CLmax'] ** 2) + J["Vstall_eas", "weight"] = ( + 1 + / np.sqrt(2 * inputs["weight"] * GRAV_CONST / rho / inputs["ac|geom|wing|S_ref"] / inputs["CLmax"]) + * GRAV_CONST + / rho + / inputs["ac|geom|wing|S_ref"] + / inputs["CLmax"] + ) + J["Vstall_eas", "ac|geom|wing|S_ref"] = -( + 1 + / np.sqrt(2 * inputs["weight"] * GRAV_CONST / rho / inputs["ac|geom|wing|S_ref"] / inputs["CLmax"]) + * inputs["weight"] + * GRAV_CONST + / rho + / inputs["ac|geom|wing|S_ref"] ** 2 + / inputs["CLmax"] + ) + J["Vstall_eas", "CLmax"] = -( + 1 + / np.sqrt(2 * inputs["weight"] * GRAV_CONST / rho / inputs["ac|geom|wing|S_ref"] / inputs["CLmax"]) + * inputs["weight"] + * GRAV_CONST + / rho + / inputs["ac|geom|wing|S_ref"] + / inputs["CLmax"] ** 2 + ) diff --git a/openconcept/aerodynamics/openaerostruct/aerostructural.py b/openconcept/aerodynamics/openaerostruct/aerostructural.py index 9c6f705a..6a51862b 100644 --- a/openconcept/aerodynamics/openaerostruct/aerostructural.py +++ b/openconcept/aerodynamics/openaerostruct/aerostructural.py @@ -518,6 +518,7 @@ def compute_partials(self, inputs, partials): partials[key][:] = value partials["CD_train", "ac|aero|CD_nonwing"] = np.ones(OASDataGen.CD.shape) + """ Generates training data and its total derivatives by calling OpenAeroStruct at each training point. @@ -583,6 +584,8 @@ def compute_partials(self, inputs, partials): Partial derivatives of the training data flattened in the proper OpenMDAO-style format for use as partial derivatives in the OASDataGen component """ + + def compute_training_data(inputs, surf_dict=None): t_start = time() print(f"Generating OpenAeroStruct aerostructural training data...") @@ -1115,7 +1118,9 @@ def setup(self): # This dummy mesh must be passed to the surface dict so OpenAeroStruct # knows the dimensions of the mesh and whether it is a left or right wing dummy_mesh = np.zeros((nx, ny, 3)) - dummy_mesh[:, :, 0], dummy_mesh[:, :, 1] = np.meshgrid(np.linspace(0, 1, nx), np.linspace(-1, 0, ny), indexing="ij") + dummy_mesh[:, :, 0], dummy_mesh[:, :, 1] = np.meshgrid( + np.linspace(0, 1, nx), np.linspace(-1, 0, ny), indexing="ij" + ) surf_dict = { # Wing definition @@ -1438,7 +1443,11 @@ def setup(self): ], ) self.promotes( - comp_name, inputs=["fltcond|alpha", "fltcond|M", "fltcond|h"], src_indices=[node], flat_src_indices=True, src_shape=(nn,) + comp_name, + inputs=["fltcond|alpha", "fltcond|M", "fltcond|h"], + src_indices=[node], + flat_src_indices=True, + src_shape=(nn,), ) # Promote wing weight from one, doesn't really matter which diff --git a/openconcept/aerodynamics/openaerostruct/drag_polar.py b/openconcept/aerodynamics/openaerostruct/drag_polar.py index 7f00eabe..c9b3b91d 100644 --- a/openconcept/aerodynamics/openaerostruct/drag_polar.py +++ b/openconcept/aerodynamics/openaerostruct/drag_polar.py @@ -451,6 +451,8 @@ def compute_partials(self, inputs, partials): Partial derivatives of the training data flattened in the proper OpenMDAO-style format for use as partial derivatives in the VLMDataGen component """ + + def compute_training_data(inputs, surf_dict=None): t_start = time() print(f"Generating OpenAeroStruct aerodynamic training data...") @@ -708,7 +710,9 @@ def setup(self): # This dummy mesh must be passed to the surface dict so OpenAeroStruct # knows the dimensions of the mesh and whether it is a left or right wing dummy_mesh = np.zeros((nx, ny, 3)) - dummy_mesh[:, :, 0], dummy_mesh[:, :, 1] = np.meshgrid(np.linspace(0, 1, nx), np.linspace(-1, 0, ny), indexing="ij") + dummy_mesh[:, :, 0], dummy_mesh[:, :, 1] = np.meshgrid( + np.linspace(0, 1, nx), np.linspace(-1, 0, ny), indexing="ij" + ) surf_dict = { "name": "wing", @@ -887,8 +891,8 @@ def compute_partials(self, inputs, J): # Compute derivatives in a way analogous to forward AD db_dS = AR / (4 * np.sqrt(AR * S)) db_dAR = S / (4 * np.sqrt(AR * S)) - dcroot_dS = 1 / (half_span * (1 + taper)) - S / (half_span ** 2 * (1 + taper)) * db_dS - dcroot_dAR = -S / (half_span ** 2 * (1 + taper)) * db_dAR + dcroot_dS = 1 / (half_span * (1 + taper)) - S / (half_span**2 * (1 + taper)) * db_dS + dcroot_dAR = -S / (half_span**2 * (1 + taper)) * db_dAR dcroot_dtaper = -S / (half_span * (1 + taper) ** 2) dy_dS = y_mesh * db_dS diff --git a/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py b/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py index 9d49e32d..e91aeb48 100644 --- a/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py +++ b/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py @@ -364,17 +364,16 @@ class AerostructDragPolarExactTestCase(unittest.TestCase): def test_defaults(self): S = 427.8 CD0 = 0.01 - q = 0.5 * 0.55427276 * 264.20682682 ** 2 + q = 0.5 * 0.55427276 * 264.20682682**2 nn = 3 p = om.Problem( - AerostructDragPolarExact( - num_nodes=nn, num_x=3, num_y=5, num_twist=2, num_toverc=2, num_skin=2, num_spar=2 - ) + AerostructDragPolarExact(num_nodes=nn, num_x=3, num_y=5, num_twist=2, num_toverc=2, num_skin=2, num_spar=2) ) p.model.nonlinear_solver = om.NewtonSolver(solve_subsystems=True, atol=1e-8, rtol=1e-10) p.model.linear_solver = om.DirectSolver() p.model.set_input_defaults( - "fltcond|CL", np.array([0.094142402327027, 0.158902999486838, 0.223695460208479]), + "fltcond|CL", + np.array([0.094142402327027, 0.158902999486838, 0.223695460208479]), ) p.model.set_input_defaults("fltcond|M", np.full(nn, 0.85)) p.model.set_input_defaults("fltcond|h", np.full(nn, 7.5e3), units="m") diff --git a/openconcept/aerodynamics/tests/test_aerodynamics.py b/openconcept/aerodynamics/tests/test_aerodynamics.py index e12b7724..f9d2f00b 100644 --- a/openconcept/aerodynamics/tests/test_aerodynamics.py +++ b/openconcept/aerodynamics/tests/test_aerodynamics.py @@ -4,23 +4,27 @@ from openmdao.api import IndepVarComp, Group, Problem from openconcept.aerodynamics import PolarDrag, Lift, StallSpeed + class PolarDragTestGroup(Group): """ This is a simple analysis group for testing the drag polar component """ + def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") + def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('conditions', IndepVarComp(),promotes_outputs=['*']) - self.add_subsystem('polardrag', PolarDrag(num_nodes=nn),promotes_inputs=['*'],promotes_outputs=['*']) + nn = self.options["num_nodes"] + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + self.add_subsystem("polardrag", PolarDrag(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + + iv.add_output("fltcond|CL", val=np.linspace(0, 1.5, nn)) + iv.add_output("fltcond|q", val=np.ones(nn) * 0.5 * 1.225 * 70**2, units="N * m**-2") + iv.add_output("ac|geom|wing|S_ref", val=30, units="m**2") + iv.add_output("ac|geom|wing|AR", val=15) + iv.add_output("CD0", val=0.02) + iv.add_output("e", val=0.8) - iv.add_output('fltcond|CL', val=np.linspace(0,1.5,nn)) - iv.add_output('fltcond|q', val=np.ones(nn)*0.5*1.225*70**2, units='N * m**-2') - iv.add_output('ac|geom|wing|S_ref', val=30, units='m**2') - iv.add_output('ac|geom|wing|AR', val=15) - iv.add_output('CD0', val=0.02) - iv.add_output('e', val=0.8) class VectorDragTestCase(unittest.TestCase): def setUp(self): @@ -29,16 +33,16 @@ def setUp(self): self.prob.run_model() def test_drag_vectorial(self): - drag_cl0 = 0.5*1.225*70**2 * 30 *(0.02 + 0**2 / np.pi / 0.8 / 15) - drag_cl1p5 = 0.5*1.225*70**2 * 30 *(0.02 + 1.5**2 / np.pi / 0.8 / 15) - assert_near_equal(self.prob['drag'][0], drag_cl0, tolerance=1e-8) - assert_near_equal(self.prob['drag'][-1], drag_cl1p5, tolerance=1e-8) - + drag_cl0 = 0.5 * 1.225 * 70**2 * 30 * (0.02 + 0**2 / np.pi / 0.8 / 15) + drag_cl1p5 = 0.5 * 1.225 * 70**2 * 30 * (0.02 + 1.5**2 / np.pi / 0.8 / 15) + assert_near_equal(self.prob["drag"][0], drag_cl0, tolerance=1e-8) + assert_near_equal(self.prob["drag"][-1], drag_cl1p5, tolerance=1e-8) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class ScalarDragTestCase(unittest.TestCase): def setUp(self): self.prob = Problem(PolarDragTestGroup(num_nodes=1)) @@ -46,27 +50,31 @@ def setUp(self): self.prob.run_model() def test_drag_scalar(self): - drag_cl0 = 0.5*1.225*70**2 * 30 *(0.02 + 0**2 / np.pi / 0.8 / 15) - assert_near_equal(self.prob['drag'], drag_cl0, tolerance=1e-8) + drag_cl0 = 0.5 * 1.225 * 70**2 * 30 * (0.02 + 0**2 / np.pi / 0.8 / 15) + assert_near_equal(self.prob["drag"], drag_cl0, tolerance=1e-8) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class LiftTestGroup(Group): """ This is a simple analysis group for testing the lift component """ + def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") + def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('conditions', IndepVarComp(),promotes_outputs=['*']) - self.add_subsystem('lift', Lift(num_nodes=nn),promotes_inputs=['*'],promotes_outputs=['*']) + nn = self.options["num_nodes"] + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + self.add_subsystem("lift", Lift(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + + iv.add_output("fltcond|CL", val=np.linspace(1.5, 0, nn)) + iv.add_output("fltcond|q", val=np.ones(nn) * 0.5 * 1.225 * 70**2, units="N * m**-2") + iv.add_output("ac|geom|wing|S_ref", val=30, units="m**2") - iv.add_output('fltcond|CL', val=np.linspace(1.5,0,nn)) - iv.add_output('fltcond|q', val=np.ones(nn)*0.5*1.225*70**2, units='N * m**-2') - iv.add_output('ac|geom|wing|S_ref', val=30, units='m**2') class VectorLiftTestCase(unittest.TestCase): def setUp(self): @@ -76,14 +84,15 @@ def setUp(self): def test_lift_vectorial(self): lift_cl0 = 0 - lift_cl1p5 = 0.5*1.225*70**2 * 30 * 1.5 - assert_near_equal(self.prob['lift'][-1], lift_cl0, tolerance=1e-8) - assert_near_equal(self.prob['lift'][0], lift_cl1p5, tolerance=1e-8) + lift_cl1p5 = 0.5 * 1.225 * 70**2 * 30 * 1.5 + assert_near_equal(self.prob["lift"][-1], lift_cl0, tolerance=1e-8) + assert_near_equal(self.prob["lift"][0], lift_cl1p5, tolerance=1e-8) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class ScalarLiftTestCase(unittest.TestCase): def setUp(self): self.prob = Problem(LiftTestGroup(num_nodes=1)) @@ -91,11 +100,11 @@ def setUp(self): self.prob.run_model() def test_lift_scalar(self): - lift_cl1p5 = 0.5*1.225*70**2 * 30 * 1.5 - assert_near_equal(self.prob['lift'], lift_cl1p5, tolerance=1e-8) + lift_cl1p5 = 0.5 * 1.225 * 70**2 * 30 * 1.5 + assert_near_equal(self.prob["lift"], lift_cl1p5, tolerance=1e-8) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) @@ -103,13 +112,15 @@ class StallSpeedTestGroup(Group): """ This is a simple analysis group for testing the stall speed component """ + def setup(self): - iv = self.add_subsystem('conditions', IndepVarComp(),promotes_outputs=['*']) - self.add_subsystem('stall', StallSpeed(),promotes_inputs=['*'],promotes_outputs=['*']) + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + self.add_subsystem("stall", StallSpeed(), promotes_inputs=["*"], promotes_outputs=["*"]) + + iv.add_output("CLmax", val=2.5) + iv.add_output("weight", val=1000, units="kg") + iv.add_output("ac|geom|wing|S_ref", val=30, units="m**2") - iv.add_output('CLmax', val=2.5) - iv.add_output('weight', val=1000, units='kg') - iv.add_output('ac|geom|wing|S_ref', val=30, units='m**2') class StallSpeedTestCase(unittest.TestCase): def setUp(self): @@ -119,8 +130,8 @@ def setUp(self): def test_stall_speed(self): vstall = np.sqrt(2 * 1000 * 9.80665 / 1.225 / 30 / 2.5) - assert_near_equal(self.prob['Vstall_eas'], vstall, tolerance=1e-8) + assert_near_equal(self.prob["Vstall_eas"], vstall, tolerance=1e-8) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) diff --git a/openconcept/atmospherics/atmospherics_data.py b/openconcept/atmospherics/atmospherics_data.py index 91e53a49..6ea20a66 100644 --- a/openconcept/atmospherics/atmospherics_data.py +++ b/openconcept/atmospherics/atmospherics_data.py @@ -1,10 +1,10 @@ -''' +""" This module provides 1976 Standard Atmosphere constants and calculations. Adapted from: J.P. Jasa, J.T. Hwang, and J.R.R.A. Martins: Design and Trajectory Optimization of a Morphing Wing Aircraft 2018 AIAA/ASCE/AHS/ASC Structures, Structural Dynamics, and Materials Conference; AIAA SciTech Forum, January 2018 -''' +""" import numpy as np @@ -13,75 +13,87 @@ h_trans = 11000 h_lower = h_trans - epsilon h_upper = h_trans + epsilon -tropopause_matrix = np.array([ - [h_lower**3, h_lower**2, h_lower, 1], - [h_upper**3, h_upper**2, h_upper, 1], - [3*h_lower**2, 2*h_lower, 1, 0], - [3*h_upper**2, 2*h_upper, 1, 0], -]) +tropopause_matrix = np.array( + [ + [h_lower**3, h_lower**2, h_lower, 1], + [h_upper**3, h_upper**2, h_upper, 1], + [3 * h_lower**2, 2 * h_lower, 1, 0], + [3 * h_upper**2, 2 * h_upper, 1, 0], + ] +) # pressure tmp1 = 1 - 0.0065 * h_lower / 288.16 -tmp2 = np.exp(-9.81*epsilon/(288*216.65)) -pressure_rhs = np.array([ - 101325 * tmp1 ** 5.2561, - 22632 * tmp2, - (-101325*5.2561*(0.0065/288.16)*tmp1**4.2561), - (22632 * (-9.81/(288*216.65)) * tmp2)]) +tmp2 = np.exp(-9.81 * epsilon / (288 * 216.65)) +pressure_rhs = np.array( + [ + 101325 * tmp1**5.2561, + 22632 * tmp2, + (-101325 * 5.2561 * (0.0065 / 288.16) * tmp1**4.2561), + (22632 * (-9.81 / (288 * 216.65)) * tmp2), + ] +) pressure_coeffs = np.linalg.solve(tropopause_matrix, pressure_rhs) # temperature -temp_rhs = np.array([ - 288.16 - (6.5e-3) * h_lower, - 216.65, - -6.5e-3, - 0, -]) +temp_rhs = np.array( + [ + 288.16 - (6.5e-3) * h_lower, + 216.65, + -6.5e-3, + 0, + ] +) temp_coeffs = np.linalg.solve(tropopause_matrix, temp_rhs) # functions def get_mask_arrays(h_m): tropos_mask = h_m <= h_lower - strato_mask = h_m > h_upper + strato_mask = h_m > h_upper smooth_mask = np.logical_and(~tropos_mask, ~strato_mask) return tropos_mask, strato_mask, smooth_mask + def compute_pressures(h_m, tropos_mask, strato_mask, smooth_mask): a, b, c, d = pressure_coeffs p_Pa = np.zeros(len(h_m), dtype=type(h_m[0])) - p_Pa += tropos_mask * (101325*(1-0.0065*h_m/288.16)**5.2561) - p_Pa += strato_mask * (22632*np.exp(-9.81*(h_m-h_trans)/(288*216.65))) - p_Pa += smooth_mask * (a*h_m**3 + b*h_m**2 + c*h_m + d) + p_Pa += tropos_mask * (101325 * (1 - 0.0065 * h_m / 288.16) ** 5.2561) + p_Pa += strato_mask * (22632 * np.exp(-9.81 * (h_m - h_trans) / (288 * 216.65))) + p_Pa += smooth_mask * (a * h_m**3 + b * h_m**2 + c * h_m + d) return p_Pa + def compute_pressure_derivs(h_m, tropos_mask, strato_mask, smooth_mask): a, b, c, d = pressure_coeffs derivs = np.zeros(len(h_m), dtype=type(h_m[0])) - derivs += tropos_mask * (101325*5.2561*(-0.0065/288.16) * (1-0.0065*h_m/288.16)**4.2561) - derivs += strato_mask * (22632*(-9.81/(288*216.65)) - *np.exp(9.81*11000/(288*216.65))*np.exp(-9.81*h_m/(288*216.65))) - derivs += smooth_mask * (3*a*h_m**2 + 2*b*h_m + c) + derivs += tropos_mask * (101325 * 5.2561 * (-0.0065 / 288.16) * (1 - 0.0065 * h_m / 288.16) ** 4.2561) + derivs += strato_mask * ( + 22632 * (-9.81 / (288 * 216.65)) * np.exp(9.81 * 11000 / (288 * 216.65)) * np.exp(-9.81 * h_m / (288 * 216.65)) + ) + derivs += smooth_mask * (3 * a * h_m**2 + 2 * b * h_m + c) return derivs + def compute_temps(h_m, tropos_mask, strato_mask, smooth_mask): a, b, c, d = temp_coeffs temp_K = np.zeros(len(h_m), dtype=type(h_m[0])) temp_K += tropos_mask * (288.16 - (6.5e-3) * h_m) temp_K += strato_mask * 216.65 - temp_K += smooth_mask * (a * h_m ** 3 + b * h_m ** 2 + c * h_m + d) + temp_K += smooth_mask * (a * h_m**3 + b * h_m**2 + c * h_m + d) return temp_K + def compute_temp_derivs(h_m, tropos_mask, strato_mask, smooth_mask): a, b, c, d = temp_coeffs derivs = np.zeros(len(h_m), dtype=type(h_m[0])) derivs += tropos_mask * (-6.5e-3) - derivs += smooth_mask * (3*a*h_m**2 + 2*b*h_m + c) + derivs += smooth_mask * (3 * a * h_m**2 + 2 * b * h_m + c) return derivs diff --git a/openconcept/atmospherics/compute_atmos_props.py b/openconcept/atmospherics/compute_atmos_props.py index 08451f66..474251a6 100644 --- a/openconcept/atmospherics/compute_atmos_props.py +++ b/openconcept/atmospherics/compute_atmos_props.py @@ -1,9 +1,18 @@ -from openconcept.atmospherics import TemperatureComp, PressureComp, DensityComp, DynamicPressureComp, TrueAirspeedComp, EquivalentAirspeedComp, SpeedOfSoundComp, MachNumberComp +from openconcept.atmospherics import ( + TemperatureComp, + PressureComp, + DensityComp, + DynamicPressureComp, + TrueAirspeedComp, + EquivalentAirspeedComp, + SpeedOfSoundComp, + MachNumberComp, +) from openmdao.api import Group class ComputeAtmosphericProperties(Group): - ''' + """ Computes pressure, density, temperature, dyn pressure, and true airspeed Inputs @@ -35,22 +44,43 @@ class ComputeAtmosphericProperties(Group): true_airspeed_in : bool Flip to true if input vector is Utrue, not Ueas. If this is true, fltcond|Utrue will be an input and fltcond|Ueas will be an output. - ''' + """ def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") - self.options.declare('true_airspeed_in',default=False,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") + self.options.declare("true_airspeed_in", default=False, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] - tas_in = self.options['true_airspeed_in'] - self.add_subsystem('temp', TemperatureComp(num_nodes=nn), promotes_inputs=['fltcond|h','fltcond|TempIncrement'], promotes_outputs=['fltcond|T']) - self.add_subsystem('pressure',PressureComp(num_nodes=nn), promotes_inputs=['fltcond|h'], promotes_outputs=['fltcond|p']) - self.add_subsystem('density',DensityComp(num_nodes=nn), promotes_inputs=['fltcond|p', 'fltcond|T'], promotes_outputs=['fltcond|rho']) - self.add_subsystem('speedofsound',SpeedOfSoundComp(num_nodes=nn), promotes_inputs=['fltcond|T'], promotes_outputs=['fltcond|a']) + nn = self.options["num_nodes"] + tas_in = self.options["true_airspeed_in"] + self.add_subsystem( + "temp", + TemperatureComp(num_nodes=nn), + promotes_inputs=["fltcond|h", "fltcond|TempIncrement"], + promotes_outputs=["fltcond|T"], + ) + self.add_subsystem( + "pressure", PressureComp(num_nodes=nn), promotes_inputs=["fltcond|h"], promotes_outputs=["fltcond|p"] + ) + self.add_subsystem( + "density", + DensityComp(num_nodes=nn), + promotes_inputs=["fltcond|p", "fltcond|T"], + promotes_outputs=["fltcond|rho"], + ) + self.add_subsystem( + "speedofsound", + SpeedOfSoundComp(num_nodes=nn), + promotes_inputs=["fltcond|T"], + promotes_outputs=["fltcond|a"], + ) if tas_in: - self.add_subsystem('equivair',EquivalentAirspeedComp(num_nodes=nn),promotes_inputs=['*'],promotes_outputs=['*']) + self.add_subsystem( + "equivair", EquivalentAirspeedComp(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) else: - self.add_subsystem('trueair',TrueAirspeedComp(num_nodes=nn),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('dynamicpressure',DynamicPressureComp(num_nodes=nn),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('machnumber',MachNumberComp(num_nodes=nn),promotes_inputs=["*"],promotes_outputs=["*"]) \ No newline at end of file + self.add_subsystem("trueair", TrueAirspeedComp(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "dynamicpressure", DynamicPressureComp(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("machnumber", MachNumberComp(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) diff --git a/openconcept/atmospherics/density_comp.py b/openconcept/atmospherics/density_comp.py index a2993751..9246e3fe 100644 --- a/openconcept/atmospherics/density_comp.py +++ b/openconcept/atmospherics/density_comp.py @@ -6,7 +6,7 @@ class DensityComp(ExplicitComponent): - ''' + """ This component computes density from pressure and temperature. Adapted from: @@ -19,7 +19,7 @@ class DensityComp(ExplicitComponent): Pressure at flight condition (vector, Pa) fltcond|T : float Temperature at flight condition (vector, K) - + Outputs ------- fltcond|rho : float @@ -29,34 +29,34 @@ class DensityComp(ExplicitComponent): ------- num_nodes : int Number of analysis points to run, i.e. length of vector inputs (scalar, dimensionless) - ''' + """ def initialize(self): - self.options.declare('num_nodes', types=int) + self.options.declare("num_nodes", types=int) def setup(self): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - self.add_input('fltcond|p', shape=num_points, units='Pa') - self.add_input('fltcond|T', shape=num_points, units='K') - self.add_output('fltcond|rho', shape=num_points, units='kg / m**3') + self.add_input("fltcond|p", shape=num_points, units="Pa") + self.add_input("fltcond|T", shape=num_points, units="K") + self.add_output("fltcond|rho", shape=num_points, units="kg / m**3") arange = np.arange(num_points) - self.declare_partials('fltcond|rho', 'fltcond|p', rows=arange, cols=arange) - self.declare_partials('fltcond|rho', 'fltcond|T', rows=arange, cols=arange) + self.declare_partials("fltcond|rho", "fltcond|p", rows=arange, cols=arange) + self.declare_partials("fltcond|rho", "fltcond|T", rows=arange, cols=arange) def compute(self, inputs, outputs): - p_Pa = inputs['fltcond|p'] - T_K = inputs['fltcond|T'] + p_Pa = inputs["fltcond|p"] + T_K = inputs["fltcond|T"] - outputs['fltcond|rho'] = p_Pa / R / T_K + outputs["fltcond|rho"] = p_Pa / R / T_K def compute_partials(self, inputs, partials): - p_Pa = inputs['fltcond|p'] - T_K = inputs['fltcond|T'] + p_Pa = inputs["fltcond|p"] + T_K = inputs["fltcond|T"] data = 1.0 / R / T_K - partials['fltcond|rho', 'fltcond|p'] = data + partials["fltcond|rho", "fltcond|p"] = data - data = -p_Pa / R / T_K ** 2 - partials['fltcond|rho', 'fltcond|T'] = data + data = -p_Pa / R / T_K**2 + partials["fltcond|rho", "fltcond|T"] = data diff --git a/openconcept/atmospherics/dynamic_pressure_comp.py b/openconcept/atmospherics/dynamic_pressure_comp.py index 2e8a8658..5279677e 100644 --- a/openconcept/atmospherics/dynamic_pressure_comp.py +++ b/openconcept/atmospherics/dynamic_pressure_comp.py @@ -4,7 +4,7 @@ class DynamicPressureComp(ExplicitComponent): - ''' + """ Calculates dynamic pressure from true airspeed and density. Inputs @@ -23,27 +23,27 @@ class DynamicPressureComp(ExplicitComponent): ------- num_nodes : int Number of analysis points to run (sets vec length) (default 1) - ''' + """ def initialize(self): - self.options.declare('num_nodes', types=int) + self.options.declare("num_nodes", types=int) def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] - self.add_input('fltcond|Utrue', units='m/s', shape=(nn,)) - self.add_input('fltcond|rho', units='kg * m**-3', shape=(nn,)) - self.add_output('fltcond|q', units='N * m**-2', shape=(nn,)) + self.add_input("fltcond|Utrue", units="m/s", shape=(nn,)) + self.add_input("fltcond|rho", units="kg * m**-3", shape=(nn,)) + self.add_output("fltcond|q", units="N * m**-2", shape=(nn,)) arange = np.arange(nn) - self.declare_partials('fltcond|q', 'fltcond|rho', rows=arange, cols=arange) - self.declare_partials('fltcond|q', 'fltcond|Utrue', rows=arange, cols=arange) + self.declare_partials("fltcond|q", "fltcond|rho", rows=arange, cols=arange) + self.declare_partials("fltcond|q", "fltcond|Utrue", rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - outputs['fltcond|q'] = 0.5 * inputs['fltcond|rho'] * inputs['fltcond|Utrue']**2 + nn = self.options["num_nodes"] + outputs["fltcond|q"] = 0.5 * inputs["fltcond|rho"] * inputs["fltcond|Utrue"] ** 2 def compute_partials(self, inputs, partials): - nn = self.options['num_nodes'] - partials['fltcond|q', 'fltcond|rho'] = 0.5 * inputs['fltcond|Utrue']**2 - partials['fltcond|q', 'fltcond|Utrue'] = inputs['fltcond|rho'] * inputs['fltcond|Utrue'] + nn = self.options["num_nodes"] + partials["fltcond|q", "fltcond|rho"] = 0.5 * inputs["fltcond|Utrue"] ** 2 + partials["fltcond|q", "fltcond|Utrue"] = inputs["fltcond|rho"] * inputs["fltcond|Utrue"] diff --git a/openconcept/atmospherics/mach_number_comp.py b/openconcept/atmospherics/mach_number_comp.py index 2c869d06..c84a51bd 100644 --- a/openconcept/atmospherics/mach_number_comp.py +++ b/openconcept/atmospherics/mach_number_comp.py @@ -2,8 +2,9 @@ from openmdao.api import ExplicitComponent + class MachNumberComp(ExplicitComponent): - ''' + """ Computes mach number from true airspeed and speed of sound Inputs @@ -22,26 +23,26 @@ class MachNumberComp(ExplicitComponent): ------- num_nodes : int Number of analysis points to run (sets vec length) (default 1) - ''' + """ def initialize(self): - self.options.declare('num_nodes', types=int) + self.options.declare("num_nodes", types=int) def setup(self): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - self.add_input('fltcond|a', units='m / s', shape=num_points) - self.add_input('fltcond|Utrue', units='m /s', shape=num_points) - self.add_output('fltcond|M', shape=num_points) + self.add_input("fltcond|a", units="m / s", shape=num_points) + self.add_input("fltcond|Utrue", units="m /s", shape=num_points) + self.add_output("fltcond|M", shape=num_points) arange = np.arange(num_points) - self.declare_partials('fltcond|M', 'fltcond|a', rows=arange, cols=arange) - self.declare_partials('fltcond|M', 'fltcond|Utrue', rows=arange, cols=arange) + self.declare_partials("fltcond|M", "fltcond|a", rows=arange, cols=arange) + self.declare_partials("fltcond|M", "fltcond|Utrue", rows=arange, cols=arange) def compute(self, inputs, outputs): - outputs['fltcond|M'] = inputs['fltcond|Utrue'] / inputs['fltcond|a'] + outputs["fltcond|M"] = inputs["fltcond|Utrue"] / inputs["fltcond|a"] def compute_partials(self, inputs, partials): - num_points = self.options['num_nodes'] - partials['fltcond|M', 'fltcond|Utrue'] = np.ones(num_points) / inputs['fltcond|a'] - partials['fltcond|M', 'fltcond|a'] = - inputs['fltcond|Utrue'] / (inputs['fltcond|a'] ** 2) \ No newline at end of file + num_points = self.options["num_nodes"] + partials["fltcond|M", "fltcond|Utrue"] = np.ones(num_points) / inputs["fltcond|a"] + partials["fltcond|M", "fltcond|a"] = -inputs["fltcond|Utrue"] / (inputs["fltcond|a"] ** 2) diff --git a/openconcept/atmospherics/pressure_comp.py b/openconcept/atmospherics/pressure_comp.py index f6febb4c..669ec0cc 100644 --- a/openconcept/atmospherics/pressure_comp.py +++ b/openconcept/atmospherics/pressure_comp.py @@ -6,7 +6,7 @@ class PressureComp(ExplicitComponent): - ''' + """ This component computes pressure from altitude using the 1976 Standard Atmosphere. Adapted from: @@ -17,45 +17,44 @@ class PressureComp(ExplicitComponent): ------ fltcond|h : float Altitude (vector, m) - + Outputs ------- fltcond|p : float Pressure at flight condition (vector, Pa) - + Options ------- num_nodes : int Number of analysis points to run, i.e. length of vector inputs (scalar, dimensionless) - ''' - + """ def initialize(self): - self.options.declare('num_nodes', types=int) + self.options.declare("num_nodes", types=int) def setup(self): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - self.add_input('fltcond|h', shape=num_points, units='m') - self.add_output('fltcond|p', shape=num_points, units='Pa') + self.add_input("fltcond|h", shape=num_points, units="m") + self.add_output("fltcond|p", shape=num_points, units="Pa") arange = np.arange(num_points) - self.declare_partials('fltcond|p', 'fltcond|h', rows=arange, cols=arange) + self.declare_partials("fltcond|p", "fltcond|h", rows=arange, cols=arange) def compute(self, inputs, outputs): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - h_m = inputs['fltcond|h'] + h_m = inputs["fltcond|h"] self.tropos_mask, self.strato_mask, self.smooth_mask = get_mask_arrays(h_m) p_Pa = compute_pressures(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) - outputs['fltcond|p'] = p_Pa + outputs["fltcond|p"] = p_Pa def compute_partials(self, inputs, partials): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - h_m = inputs['fltcond|h'] + h_m = inputs["fltcond|h"] derivs = compute_pressure_derivs(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) - partials['fltcond|p', 'fltcond|h'] = derivs + partials["fltcond|p", "fltcond|h"] = derivs diff --git a/openconcept/atmospherics/speedofsound_comp.py b/openconcept/atmospherics/speedofsound_comp.py index fcf35aa7..5baf44ec 100644 --- a/openconcept/atmospherics/speedofsound_comp.py +++ b/openconcept/atmospherics/speedofsound_comp.py @@ -8,7 +8,7 @@ class SpeedOfSoundComp(ExplicitComponent): - ''' + """ This component computes speed of sound from temperature. Adapted from: @@ -19,38 +19,37 @@ class SpeedOfSoundComp(ExplicitComponent): ------ fltcond|T : float Temperature at flight condition (vector, K) - + Outputs ------- fltcond|a : float Speed of sound (vector, m/s) - + Options ------- num_nodes : int Number of analysis points to run, i.e. length of vector inputs (scalar, dimensionless) - ''' - + """ def initialize(self): - self.options.declare('num_nodes', types=int) + self.options.declare("num_nodes", types=int) def setup(self): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - self.add_input('fltcond|T', shape=num_points, units='K') - self.add_output('fltcond|a', shape=num_points, units='m/s') + self.add_input("fltcond|T", shape=num_points, units="K") + self.add_output("fltcond|a", shape=num_points, units="m/s") arange = np.arange(num_points) - self.declare_partials('fltcond|a', 'fltcond|T', rows=arange, cols=arange) + self.declare_partials("fltcond|a", "fltcond|T", rows=arange, cols=arange) def compute(self, inputs, outputs): - T_K = inputs['fltcond|T'] + T_K = inputs["fltcond|T"] - outputs['fltcond|a'] = np.sqrt(gamma * R * T_K) + outputs["fltcond|a"] = np.sqrt(gamma * R * T_K) def compute_partials(self, inputs, partials): - T_K = inputs['fltcond|T'] + T_K = inputs["fltcond|T"] data = 0.5 * np.sqrt(gamma * R / T_K) - partials['fltcond|a', 'fltcond|T'] = data + partials["fltcond|a", "fltcond|T"] = data diff --git a/openconcept/atmospherics/temperature_comp.py b/openconcept/atmospherics/temperature_comp.py index b490ce2a..af509800 100644 --- a/openconcept/atmospherics/temperature_comp.py +++ b/openconcept/atmospherics/temperature_comp.py @@ -6,7 +6,7 @@ class TemperatureComp(ExplicitComponent): - ''' + """ This component computes temperature from altitude using the 1976 Standard Atmosphere. Adapted from: @@ -19,48 +19,47 @@ class TemperatureComp(ExplicitComponent): Altitude (vector, m) fltcond|TempIncrement : float Offset for temperature; useful for modeling hot (+ increment) or cold (- increment) days (vector, deg C) - + Outputs ------- fltcond|T : float Temperature at flight condition (vector, K) - + Options ------- num_nodes : int Number of analysis points to run, i.e. length of vector inputs (scalar, dimensionless) - ''' - + """ def initialize(self): - self.options.declare('num_nodes', types=int) + self.options.declare("num_nodes", types=int) def setup(self): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - self.add_input('fltcond|h', shape=num_points, units='m') - self.add_input('fltcond|TempIncrement', shape=num_points, val=0.0, units='degC') - self.add_output('fltcond|T', shape=num_points, lower=0., units='K') + self.add_input("fltcond|h", shape=num_points, units="m") + self.add_input("fltcond|TempIncrement", shape=num_points, val=0.0, units="degC") + self.add_output("fltcond|T", shape=num_points, lower=0.0, units="K") arange = np.arange(num_points) - self.declare_partials('fltcond|T', 'fltcond|h', rows=arange, cols=arange) - self.declare_partials('fltcond|T', 'fltcond|TempIncrement', rows=arange, cols=arange, val=1.0) + self.declare_partials("fltcond|T", "fltcond|h", rows=arange, cols=arange) + self.declare_partials("fltcond|T", "fltcond|TempIncrement", rows=arange, cols=arange, val=1.0) def compute(self, inputs, outputs): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - h_m = inputs['fltcond|h'] + h_m = inputs["fltcond|h"] self.tropos_mask, self.strato_mask, self.smooth_mask = get_mask_arrays(h_m) temp_K = compute_temps(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) - outputs['fltcond|T'] = temp_K + inputs['fltcond|TempIncrement'] + outputs["fltcond|T"] = temp_K + inputs["fltcond|TempIncrement"] def compute_partials(self, inputs, partials): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - h_m = inputs['fltcond|h'] + h_m = inputs["fltcond|h"] derivs = compute_temp_derivs(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) - partials['fltcond|T', 'fltcond|h'] = derivs + partials["fltcond|T", "fltcond|h"] = derivs diff --git a/openconcept/atmospherics/tests/test_atmospherics.py b/openconcept/atmospherics/tests/test_atmospherics.py index 25268db5..ec9939df 100644 --- a/openconcept/atmospherics/tests/test_atmospherics.py +++ b/openconcept/atmospherics/tests/test_atmospherics.py @@ -4,19 +4,22 @@ from openmdao.api import IndepVarComp, Group, Problem from openconcept.atmospherics import ComputeAtmosphericProperties + class AtmosTestGroup(Group): - """This computes pressure, temperature, and density for a given altitude at ISA condtions. Also true airspeed from equivalent ~ indicated airspeed - """ + """This computes pressure, temperature, and density for a given altitude at ISA condtions. Also true airspeed from equivalent ~ indicated airspeed""" + def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") + def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('conditions', IndepVarComp()) - self.add_subsystem('atmos', ComputeAtmosphericProperties(num_nodes=nn),promotes_outputs=['*']) - iv.add_output('h', val=np.linspace(0,30000,nn), units='ft') - iv.add_output('Ueas', val=np.ones(nn)*120, units='kn') - self.connect('conditions.h','atmos.fltcond|h') - self.connect('conditions.Ueas','atmos.fltcond|Ueas') + nn = self.options["num_nodes"] + iv = self.add_subsystem("conditions", IndepVarComp()) + self.add_subsystem("atmos", ComputeAtmosphericProperties(num_nodes=nn), promotes_outputs=["*"]) + iv.add_output("h", val=np.linspace(0, 30000, nn), units="ft") + iv.add_output("Ueas", val=np.ones(nn) * 120, units="kn") + self.connect("conditions.h", "atmos.fltcond|h") + self.connect("conditions.Ueas", "atmos.fltcond|Ueas") + class VectorAtmosTestCase(unittest.TestCase): def setUp(self): @@ -25,50 +28,50 @@ def setUp(self): self.prob.run_model() def test_sea_level_and_30kft(self): - #check conditions at sea level - assert_near_equal(self.prob['fltcond|rho'][0],1.225,tolerance=1e-4) - assert_near_equal(self.prob['fltcond|p'][0],101325,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|T'][0],288.15,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|Utrue'][0],61.7333,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|q'][0],2334.2398,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|M'][0],0.1814,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|a'][0],340.294,tolerance=1e-3) + # check conditions at sea level + assert_near_equal(self.prob["fltcond|rho"][0], 1.225, tolerance=1e-4) + assert_near_equal(self.prob["fltcond|p"][0], 101325, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|T"][0], 288.15, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|Utrue"][0], 61.7333, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|q"][0], 2334.2398, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|M"][0], 0.1814, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|a"][0], 340.294, tolerance=1e-3) - #check conditions at 30kft (1976 standard atmosphere verified at https://www.digitaldutch.com/atmoscalc/) - assert_near_equal(self.prob['fltcond|rho'][-1],0.458312,tolerance=1e-4) - assert_near_equal(self.prob['fltcond|p'][-1],30089.6,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|T'][-1],228.714,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|Utrue'][-1],61.7333*np.sqrt(1.225/0.458312),tolerance=1e-3) - assert_near_equal(self.prob['fltcond|q'][-1],2334.2398,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|M'][-1],0.3326,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|a'][-1],303.2301,tolerance=1e-3) + # check conditions at 30kft (1976 standard atmosphere verified at https://www.digitaldutch.com/atmoscalc/) + assert_near_equal(self.prob["fltcond|rho"][-1], 0.458312, tolerance=1e-4) + assert_near_equal(self.prob["fltcond|p"][-1], 30089.6, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|T"][-1], 228.714, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|Utrue"][-1], 61.7333 * np.sqrt(1.225 / 0.458312), tolerance=1e-3) + assert_near_equal(self.prob["fltcond|q"][-1], 2334.2398, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|M"][-1], 0.3326, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|a"][-1], 303.2301, tolerance=1e-3) def test_ISA_temp_offset(self): - self.prob.set_val('atmos.fltcond|TempIncrement', np.linspace(30,15,5), units='degC') + self.prob.set_val("atmos.fltcond|TempIncrement", np.linspace(30, 15, 5), units="degC") self.prob.run_model() - #check conditions at sea level - assert_near_equal(self.prob['fltcond|rho'][0],1.10949,tolerance=1e-4) - assert_near_equal(self.prob['fltcond|p'][0],101325,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|T'][0],318.150,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|Utrue'][0],64.8674,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|q'][0],2334.2398,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|M'][0],0.1814,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|a'][0],357.5698,tolerance=1e-3) + # check conditions at sea level + assert_near_equal(self.prob["fltcond|rho"][0], 1.10949, tolerance=1e-4) + assert_near_equal(self.prob["fltcond|p"][0], 101325, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|T"][0], 318.150, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|Utrue"][0], 64.8674, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|q"][0], 2334.2398, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|M"][0], 0.1814, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|a"][0], 357.5698, tolerance=1e-3) # #check conditions at 30kft (1976 standard atmosphere verified at https://www.digitaldutch.com/atmoscalc/) - assert_near_equal(self.prob['fltcond|rho'][-1],0.430104,tolerance=1e-4) - assert_near_equal(self.prob['fltcond|p'][-1],30089.6,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|T'][-1],243.714,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|Utrue'][-1],61.7333*np.sqrt(1.225/0.430104),tolerance=1e-3) - assert_near_equal(self.prob['fltcond|q'][-1],2334.2398,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|M'][-1],61.7333*np.sqrt(1.225/0.430104)/312.957,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|a'][-1],312.957,tolerance=1e-3) - + assert_near_equal(self.prob["fltcond|rho"][-1], 0.430104, tolerance=1e-4) + assert_near_equal(self.prob["fltcond|p"][-1], 30089.6, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|T"][-1], 243.714, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|Utrue"][-1], 61.7333 * np.sqrt(1.225 / 0.430104), tolerance=1e-3) + assert_near_equal(self.prob["fltcond|q"][-1], 2334.2398, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|M"][-1], 61.7333 * np.sqrt(1.225 / 0.430104) / 312.957, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|a"][-1], 312.957, tolerance=1e-3) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class ScalarAtmosTestCase(unittest.TestCase): def setUp(self): self.prob = Problem(AtmosTestGroup(num_nodes=1)) @@ -76,14 +79,14 @@ def setUp(self): self.prob.run_model() def test_sea_level(self): - assert_near_equal(self.prob['fltcond|rho'][0],1.225,tolerance=1e-4) - assert_near_equal(self.prob['fltcond|p'][0],101325,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|T'][0],288.15,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|Utrue'][0],61.7333,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|q'][0],2334.2398,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|M'][0],0.1814,tolerance=1e-3) - assert_near_equal(self.prob['fltcond|a'][0],340.294,tolerance=1e-3) + assert_near_equal(self.prob["fltcond|rho"][0], 1.225, tolerance=1e-4) + assert_near_equal(self.prob["fltcond|p"][0], 101325, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|T"][0], 288.15, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|Utrue"][0], 61.7333, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|q"][0], 2334.2398, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|M"][0], 0.1814, tolerance=1e-3) + assert_near_equal(self.prob["fltcond|a"][0], 340.294, tolerance=1e-3) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) - assert_check_partials(partials) \ No newline at end of file + partials = self.prob.check_partials(method="cs", out_stream=None) + assert_check_partials(partials) diff --git a/openconcept/atmospherics/true_airspeed.py b/openconcept/atmospherics/true_airspeed.py index 88faf3c1..c2bcf982 100644 --- a/openconcept/atmospherics/true_airspeed.py +++ b/openconcept/atmospherics/true_airspeed.py @@ -2,8 +2,9 @@ from openmdao.api import ExplicitComponent + class TrueAirspeedComp(ExplicitComponent): - ''' + """ Computes true airspeed from equivalent airspeed and density Inputs @@ -22,33 +23,36 @@ class TrueAirspeedComp(ExplicitComponent): ------- num_nodes : int Number of analysis points to run (sets vec length) (default 1) - ''' + """ def initialize(self): - self.options.declare('num_nodes', types=int) + self.options.declare("num_nodes", types=int) def setup(self): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - self.add_input('fltcond|Ueas', units='m / s', shape=num_points) - self.add_input('fltcond|rho', units='kg * m**-3', shape=num_points) - self.add_output('fltcond|Utrue', units='m / s', shape=num_points) + self.add_input("fltcond|Ueas", units="m / s", shape=num_points) + self.add_input("fltcond|rho", units="kg * m**-3", shape=num_points) + self.add_output("fltcond|Utrue", units="m / s", shape=num_points) arange = np.arange(num_points) - self.declare_partials('fltcond|Utrue', 'fltcond|Ueas', rows=arange, cols=arange) - self.declare_partials('fltcond|Utrue', 'fltcond|rho', rows=arange, cols=arange) + self.declare_partials("fltcond|Utrue", "fltcond|Ueas", rows=arange, cols=arange) + self.declare_partials("fltcond|Utrue", "fltcond|rho", rows=arange, cols=arange) def compute(self, inputs, outputs): rho_isa_kgm3 = 1.225 - outputs['fltcond|Utrue'] = inputs['fltcond|Ueas']*np.sqrt(rho_isa_kgm3/inputs['fltcond|rho']) + outputs["fltcond|Utrue"] = inputs["fltcond|Ueas"] * np.sqrt(rho_isa_kgm3 / inputs["fltcond|rho"]) def compute_partials(self, inputs, partials): rho_isa_kgm3 = 1.225 - partials['fltcond|Utrue', 'fltcond|Ueas'] = np.sqrt(rho_isa_kgm3/inputs['fltcond|rho']) - partials['fltcond|Utrue', 'fltcond|rho'] = inputs['fltcond|Ueas']*np.sqrt(rho_isa_kgm3)*(-1/2)*inputs['fltcond|rho']**(-3/2) + partials["fltcond|Utrue", "fltcond|Ueas"] = np.sqrt(rho_isa_kgm3 / inputs["fltcond|rho"]) + partials["fltcond|Utrue", "fltcond|rho"] = ( + inputs["fltcond|Ueas"] * np.sqrt(rho_isa_kgm3) * (-1 / 2) * inputs["fltcond|rho"] ** (-3 / 2) + ) + class EquivalentAirspeedComp(ExplicitComponent): - ''' + """ Computes equivalent airspeed from true airspeed and density Inputs @@ -67,27 +71,29 @@ class EquivalentAirspeedComp(ExplicitComponent): ------- num_nodes : int Number of analysis points to run (sets vec length) (default 1) - ''' + """ def initialize(self): - self.options.declare('num_nodes', types=int) + self.options.declare("num_nodes", types=int) def setup(self): - num_points = self.options['num_nodes'] + num_points = self.options["num_nodes"] - self.add_output('fltcond|Ueas', units='m / s', shape=num_points) - self.add_input('fltcond|rho', units='kg * m**-3', shape=num_points) - self.add_input('fltcond|Utrue', units='m / s', shape=num_points) + self.add_output("fltcond|Ueas", units="m / s", shape=num_points) + self.add_input("fltcond|rho", units="kg * m**-3", shape=num_points) + self.add_input("fltcond|Utrue", units="m / s", shape=num_points) arange = np.arange(num_points) - self.declare_partials('fltcond|Ueas', 'fltcond|Utrue', rows=arange, cols=arange) - self.declare_partials('fltcond|Ueas', 'fltcond|rho', rows=arange, cols=arange) + self.declare_partials("fltcond|Ueas", "fltcond|Utrue", rows=arange, cols=arange) + self.declare_partials("fltcond|Ueas", "fltcond|rho", rows=arange, cols=arange) def compute(self, inputs, outputs): rho_isa_kgm3 = 1.225 - outputs['fltcond|Ueas'] = inputs['fltcond|Utrue']*np.sqrt(inputs['fltcond|rho'] / rho_isa_kgm3) + outputs["fltcond|Ueas"] = inputs["fltcond|Utrue"] * np.sqrt(inputs["fltcond|rho"] / rho_isa_kgm3) def compute_partials(self, inputs, partials): rho_isa_kgm3 = 1.225 - partials['fltcond|Ueas', 'fltcond|Utrue'] = np.sqrt(inputs['fltcond|rho'] / rho_isa_kgm3) - partials['fltcond|Ueas', 'fltcond|rho'] = (1/2)*inputs['fltcond|Utrue']*(inputs['fltcond|rho'] / rho_isa_kgm3)**(-1/2) / rho_isa_kgm3 + partials["fltcond|Ueas", "fltcond|Utrue"] = np.sqrt(inputs["fltcond|rho"] / rho_isa_kgm3) + partials["fltcond|Ueas", "fltcond|rho"] = ( + (1 / 2) * inputs["fltcond|Utrue"] * (inputs["fltcond|rho"] / rho_isa_kgm3) ** (-1 / 2) / rho_isa_kgm3 + ) diff --git a/openconcept/costs/__init__.py b/openconcept/costs/__init__.py index b60213a4..437f7202 100644 --- a/openconcept/costs/__init__.py +++ b/openconcept/costs/__init__.py @@ -1 +1 @@ -from .costs_commuter import TurbopropOperatingCost \ No newline at end of file +from .costs_commuter import TurbopropOperatingCost diff --git a/openconcept/costs/costs_commuter.py b/openconcept/costs/costs_commuter.py index a59747cb..0140d38a 100644 --- a/openconcept/costs/costs_commuter.py +++ b/openconcept/costs/costs_commuter.py @@ -9,102 +9,149 @@ class TurbopropOperatingCost(ExplicitComponent): def initialize(self): - self.options.declare('n_components',default=1,desc='Number of propulsion components, e.g. engines, motors, generators. Inputs will be numbered "component_1" thru n ') - self.options.declare('n_batteries',default=None,desc='Number of batteries. These should NOT be counted as components as they are not to be subtracted from OEW. Numbered "battery_1" through n') - self.options.declare('airframe_cost_per_kg',default=266.0,desc='Cost of each kg of airplane empty weight MINUS ENGINES/MOTORS/GEN/BATTERIES') - self.options.declare('fuel_cost_per_kg',default=2.5/3.08,desc='Cost of each KG of fuel; divide USD/gal by 3.08kg/gal') - self.options.declare('electricity_cost_per_MJ',default=0.01,desc='Electricity cost per MJ. Divide MWh cost by 3600 MJ/MWh') - self.options.declare('aircraft_life_years',default=15,desc='Useful aircraft life in years') - self.options.declare('aircraft_daily_cycles',default=5,desc='Number of mission cycles per day (average).') - self.options.declare('battery_replacement_cycles',default=1500,desc='Number of battery cycles until replacement') - self.options.declare('OEM_premium',default=1.1,desc='Multiplier on the airframe cost as profit margin for the OEM. 10% margin = 1.1') + self.options.declare( + "n_components", + default=1, + desc='Number of propulsion components, e.g. engines, motors, generators. Inputs will be numbered "component_1" thru n ', + ) + self.options.declare( + "n_batteries", + default=None, + desc='Number of batteries. These should NOT be counted as components as they are not to be subtracted from OEW. Numbered "battery_1" through n', + ) + self.options.declare( + "airframe_cost_per_kg", + default=266.0, + desc="Cost of each kg of airplane empty weight MINUS ENGINES/MOTORS/GEN/BATTERIES", + ) + self.options.declare( + "fuel_cost_per_kg", default=2.5 / 3.08, desc="Cost of each KG of fuel; divide USD/gal by 3.08kg/gal" + ) + self.options.declare( + "electricity_cost_per_MJ", default=0.01, desc="Electricity cost per MJ. Divide MWh cost by 3600 MJ/MWh" + ) + self.options.declare("aircraft_life_years", default=15, desc="Useful aircraft life in years") + self.options.declare("aircraft_daily_cycles", default=5, desc="Number of mission cycles per day (average).") + self.options.declare( + "battery_replacement_cycles", default=1500, desc="Number of battery cycles until replacement" + ) + self.options.declare( + "OEM_premium", + default=1.1, + desc="Multiplier on the airframe cost as profit margin for the OEM. 10% margin = 1.1", + ) def setup(self): - n_components = self.options['n_components'] - n_batteries = self.options['n_batteries'] - battery_replacement_cycles = self.options['battery_replacement_cycles'] - af_cost_kg = self.options['airframe_cost_per_kg'] - aircraft_daily_cycles = self.options['aircraft_daily_cycles'] - aircraft_life_years = self.options['aircraft_life_years'] - fuel_cost_per_kg = self.options['fuel_cost_per_kg'] - electricity_cost_per_MJ = self.options['electricity_cost_per_MJ'] - OEM_premium = self.options['OEM_premium'] + n_components = self.options["n_components"] + n_batteries = self.options["n_batteries"] + battery_replacement_cycles = self.options["battery_replacement_cycles"] + af_cost_kg = self.options["airframe_cost_per_kg"] + aircraft_daily_cycles = self.options["aircraft_daily_cycles"] + aircraft_life_years = self.options["aircraft_life_years"] + fuel_cost_per_kg = self.options["fuel_cost_per_kg"] + electricity_cost_per_MJ = self.options["electricity_cost_per_MJ"] + OEM_premium = self.options["OEM_premium"] for i in range(n_components): - self.add_input('component_'+str(i+1)+'_weight', units='kg',desc='Component weight') - self.add_input('component_'+str(i+1)+'_NR_cost', units='USD',desc='Component cost') - self.declare_partials(['airframe_NR_cost','total_NR_cost'], ['component_'+str(i+1)+'_weight'], val= - af_cost_kg * OEM_premium) - self.declare_partials(['trip_direct_operating_cost'], ['component_'+str(i+1)+'_weight'], val= - af_cost_kg * OEM_premium / aircraft_daily_cycles / 365 / aircraft_life_years) - self.declare_partials(['total_NR_cost'], ['component_'+str(i+1)+'_NR_cost'], val= OEM_premium) - self.declare_partials(['trip_direct_operating_cost'], ['component_'+str(i+1)+'_NR_cost'], val= OEM_premium/ aircraft_daily_cycles / 365 / aircraft_life_years) - - + self.add_input("component_" + str(i + 1) + "_weight", units="kg", desc="Component weight") + self.add_input("component_" + str(i + 1) + "_NR_cost", units="USD", desc="Component cost") + self.declare_partials( + ["airframe_NR_cost", "total_NR_cost"], + ["component_" + str(i + 1) + "_weight"], + val=-af_cost_kg * OEM_premium, + ) + self.declare_partials( + ["trip_direct_operating_cost"], + ["component_" + str(i + 1) + "_weight"], + val=-af_cost_kg * OEM_premium / aircraft_daily_cycles / 365 / aircraft_life_years, + ) + self.declare_partials(["total_NR_cost"], ["component_" + str(i + 1) + "_NR_cost"], val=OEM_premium) + self.declare_partials( + ["trip_direct_operating_cost"], + ["component_" + str(i + 1) + "_NR_cost"], + val=OEM_premium / aircraft_daily_cycles / 365 / aircraft_life_years, + ) if n_batteries is not None: - self.add_output('trip_battery_cost', units='USD') - self.add_output('electricity_cost', units='USD') + self.add_output("trip_battery_cost", units="USD") + self.add_output("electricity_cost", units="USD") for i in range(n_batteries): - self.add_input('battery_'+str(i+1)+'_NR_cost', units='USD',desc='Battery purchase cost') - self.add_input('battery_'+str(i+1)+'_energy_used', units='MJ',desc='Battery energy used for mission') - self.declare_partials(['electricity_cost','trip_energy_cost','trip_direct_operating_cost'], ['battery_'+str(i+1)+'_energy_used'], val=electricity_cost_per_MJ) - self.declare_partials(['trip_battery_cost','trip_direct_operating_cost'], ['battery_'+str(i+1)+'_NR_cost'], val= 1 / battery_replacement_cycles ) - - self.add_input('fuel_burn', units='kg',desc="Fuel used for mission") - self.add_input('OEW', units='kg',desc='Operating empty weight') - - self.add_output('fuel_cost', units='USD') - self.add_output('airframe_NR_cost', units='USD') - self.add_output('total_NR_cost', units='USD') - self.add_output('trip_energy_cost', units='USD') - self.add_output('trip_direct_operating_cost', units='USD') - - self.declare_partials(['airframe_NR_cost','total_NR_cost'], ['OEW'], val=af_cost_kg * OEM_premium) - self.declare_partials(['trip_direct_operating_cost'], ['OEW'], val=af_cost_kg * OEM_premium / aircraft_daily_cycles / 365 / aircraft_life_years) - self.declare_partials(['fuel_cost','trip_energy_cost','trip_direct_operating_cost'], ['fuel_burn'], val=fuel_cost_per_kg) - + self.add_input("battery_" + str(i + 1) + "_NR_cost", units="USD", desc="Battery purchase cost") + self.add_input( + "battery_" + str(i + 1) + "_energy_used", units="MJ", desc="Battery energy used for mission" + ) + self.declare_partials( + ["electricity_cost", "trip_energy_cost", "trip_direct_operating_cost"], + ["battery_" + str(i + 1) + "_energy_used"], + val=electricity_cost_per_MJ, + ) + self.declare_partials( + ["trip_battery_cost", "trip_direct_operating_cost"], + ["battery_" + str(i + 1) + "_NR_cost"], + val=1 / battery_replacement_cycles, + ) + + self.add_input("fuel_burn", units="kg", desc="Fuel used for mission") + self.add_input("OEW", units="kg", desc="Operating empty weight") + + self.add_output("fuel_cost", units="USD") + self.add_output("airframe_NR_cost", units="USD") + self.add_output("total_NR_cost", units="USD") + self.add_output("trip_energy_cost", units="USD") + self.add_output("trip_direct_operating_cost", units="USD") + + self.declare_partials(["airframe_NR_cost", "total_NR_cost"], ["OEW"], val=af_cost_kg * OEM_premium) + self.declare_partials( + ["trip_direct_operating_cost"], + ["OEW"], + val=af_cost_kg * OEM_premium / aircraft_daily_cycles / 365 / aircraft_life_years, + ) + self.declare_partials( + ["fuel_cost", "trip_energy_cost", "trip_direct_operating_cost"], ["fuel_burn"], val=fuel_cost_per_kg + ) def compute(self, inputs, outputs): - #compute empty weight less the number of propulsion components + # compute empty weight less the number of propulsion components # OEW - w_component for n components - n_components = self.options['n_components'] - n_batteries = self.options['n_batteries'] - battery_replacement_cycles = self.options['battery_replacement_cycles'] - af_cost_kg = self.options['airframe_cost_per_kg'] - aircraft_daily_cycles = self.options['aircraft_daily_cycles'] - aircraft_life_years = self.options['aircraft_life_years'] - fuel_cost_per_kg = self.options['fuel_cost_per_kg'] - electricity_cost_per_MJ = self.options['electricity_cost_per_MJ'] - OEM_premium = self.options['OEM_premium'] - - adj_OEW = inputs['OEW'] + n_components = self.options["n_components"] + n_batteries = self.options["n_batteries"] + battery_replacement_cycles = self.options["battery_replacement_cycles"] + af_cost_kg = self.options["airframe_cost_per_kg"] + aircraft_daily_cycles = self.options["aircraft_daily_cycles"] + aircraft_life_years = self.options["aircraft_life_years"] + fuel_cost_per_kg = self.options["fuel_cost_per_kg"] + electricity_cost_per_MJ = self.options["electricity_cost_per_MJ"] + OEM_premium = self.options["OEM_premium"] + + adj_OEW = inputs["OEW"] components_NR_cost = 0 for i in range(n_components): - adj_OEW = adj_OEW - inputs['component_'+str(i+1)+'_weight'] - components_NR_cost = components_NR_cost + inputs['component_'+str(i+1)+'_NR_cost'] + adj_OEW = adj_OEW - inputs["component_" + str(i + 1) + "_weight"] + components_NR_cost = components_NR_cost + inputs["component_" + str(i + 1) + "_NR_cost"] - outputs['airframe_NR_cost'] = adj_OEW * af_cost_kg * OEM_premium - outputs['total_NR_cost'] = outputs['airframe_NR_cost'] + components_NR_cost *OEM_premium + outputs["airframe_NR_cost"] = adj_OEW * af_cost_kg * OEM_premium + outputs["total_NR_cost"] = outputs["airframe_NR_cost"] + components_NR_cost * OEM_premium - NR_contrib_trip_cost = outputs['total_NR_cost'] / aircraft_daily_cycles / 365 / aircraft_life_years + NR_contrib_trip_cost = outputs["total_NR_cost"] / aircraft_daily_cycles / 365 / aircraft_life_years - outputs['fuel_cost'] = inputs['fuel_burn'] * fuel_cost_per_kg + outputs["fuel_cost"] = inputs["fuel_burn"] * fuel_cost_per_kg total_elec = 0 total_batt_cost = 0 if n_batteries is not None: for i in range(n_batteries): - total_elec = total_elec + inputs['battery_'+str(i+1)+'_energy_used'] - total_batt_cost = total_batt_cost + inputs['battery_'+str(i+1)+'_NR_cost'] - outputs['electricity_cost'] = total_elec * electricity_cost_per_MJ - outputs['trip_battery_cost'] = total_batt_cost / battery_replacement_cycles - outputs['trip_energy_cost'] = outputs['electricity_cost'] + outputs['fuel_cost'] - outputs['trip_direct_operating_cost'] = outputs['trip_energy_cost'] + NR_contrib_trip_cost + outputs['trip_battery_cost'] + total_elec = total_elec + inputs["battery_" + str(i + 1) + "_energy_used"] + total_batt_cost = total_batt_cost + inputs["battery_" + str(i + 1) + "_NR_cost"] + outputs["electricity_cost"] = total_elec * electricity_cost_per_MJ + outputs["trip_battery_cost"] = total_batt_cost / battery_replacement_cycles + outputs["trip_energy_cost"] = outputs["electricity_cost"] + outputs["fuel_cost"] + outputs["trip_direct_operating_cost"] = ( + outputs["trip_energy_cost"] + NR_contrib_trip_cost + outputs["trip_battery_cost"] + ) else: - outputs['trip_energy_cost'] = outputs['fuel_cost'] - outputs['trip_direct_operating_cost'] = outputs['trip_energy_cost'] + NR_contrib_trip_cost - - + outputs["trip_energy_cost"] = outputs["fuel_cost"] + outputs["trip_direct_operating_cost"] = outputs["trip_energy_cost"] + NR_contrib_trip_cost # def compute_partials(self, inputs, J): # n_components = self.options['n_components'] diff --git a/openconcept/energy_storage/battery.py b/openconcept/energy_storage/battery.py index 5bb3ffa9..ef60b254 100644 --- a/openconcept/energy_storage/battery.py +++ b/openconcept/energy_storage/battery.py @@ -2,6 +2,7 @@ from openmdao.api import ExplicitComponent, Group from openconcept.utilities import ElementMultiplyDivideComp, Integrator + class SOCBattery(Group): """ Same as SimpleBattery but also tracks state of charge @@ -46,35 +47,69 @@ class SOCBattery(Group): cost_base : float Base cost (default 1 USD) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('efficiency', default=1., desc='Efficiency (dimensionless)') - self.options.declare('specific_power', default=5000., desc='Battery specific power (W/kg)') - self.options.declare('specific_energy', default=300., desc='Battery spec energy') - self.options.declare('cost_inc', default=50., desc='$ cost per kg') - self.options.declare('cost_base', default=1., desc='$ cost base') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("efficiency", default=1.0, desc="Efficiency (dimensionless)") + self.options.declare("specific_power", default=5000.0, desc="Battery specific power (W/kg)") + self.options.declare("specific_energy", default=300.0, desc="Battery spec energy") + self.options.declare("cost_inc", default=50.0, desc="$ cost per kg") + self.options.declare("cost_base", default=1.0, desc="$ cost base") def setup(self): - nn = self.options['num_nodes'] - - eta_b = self.options['efficiency'] - e_b = self.options['specific_energy'] - p_b = self.options['specific_power'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] - - self.add_subsystem('batt_base',SimpleBattery(num_nodes=nn, efficiency=eta_b, specific_energy=e_b, - specific_power=p_b, cost_inc=cost_inc, cost_base=cost_base), - promotes_outputs=['*'],promotes_inputs=['*']) - + nn = self.options["num_nodes"] + + eta_b = self.options["efficiency"] + e_b = self.options["specific_energy"] + p_b = self.options["specific_power"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] + + self.add_subsystem( + "batt_base", + SimpleBattery( + num_nodes=nn, + efficiency=eta_b, + specific_energy=e_b, + specific_power=p_b, + cost_inc=cost_inc, + cost_base=cost_base, + ), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) # change in SOC over time is (- elec_load) / max_energy - self.add_subsystem('divider',ElementMultiplyDivideComp(output_name='dSOCdt',input_names=['elec_load','max_energy'],vec_size=[nn,1],scaling_factor=-1,divide=[False,True],input_units=['W','kJ']), - promotes_inputs=['*'],promotes_outputs=['*']) - - integ = self.add_subsystem('ode_integ', Integrator(num_nodes=nn, method='simpson', diff_units='s', time_setup='duration'), promotes_inputs=['*'], promotes_outputs=['*']) - integ.add_integrand('SOC', rate_name='dSOCdt', start_name='SOC_initial', end_name='SOC_final', units=None, val=1.0, start_val=1.0) + self.add_subsystem( + "divider", + ElementMultiplyDivideComp( + output_name="dSOCdt", + input_names=["elec_load", "max_energy"], + vec_size=[nn, 1], + scaling_factor=-1, + divide=[False, True], + input_units=["W", "kJ"], + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + + integ = self.add_subsystem( + "ode_integ", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + integ.add_integrand( + "SOC", + rate_name="dSOCdt", + start_name="SOC_initial", + end_name="SOC_final", + units=None, + val=1.0, + start_val=1.0, + ) class SimpleBattery(ExplicitComponent): @@ -118,57 +153,54 @@ class SimpleBattery(ExplicitComponent): cost_base : float Base cost (default 1 USD) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('efficiency', default=1., desc='Efficiency (dimensionless)') - self.options.declare('specific_power', default=5000., desc='Battery specific power (W/kg)') - self.options.declare('specific_energy', default=300., desc='Battery spec energy') - self.options.declare('cost_inc', default=50., desc='$ cost per kg') - self.options.declare('cost_base', default=1., desc='$ cost base') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("efficiency", default=1.0, desc="Efficiency (dimensionless)") + self.options.declare("specific_power", default=5000.0, desc="Battery specific power (W/kg)") + self.options.declare("specific_energy", default=300.0, desc="Battery spec energy") + self.options.declare("cost_inc", default=50.0, desc="$ cost per kg") + self.options.declare("cost_base", default=1.0, desc="$ cost base") def setup(self): - nn = self.options['num_nodes'] - self.add_input('battery_weight', units='kg', desc='Total battery pack weight') - self.add_input('elec_load', units='W', desc='Electrical load drawn', shape=(nn,)) - e_b = self.options['specific_energy'] - self.add_input('specific_energy', units='W * h / kg', val=e_b) - eta_b = self.options['efficiency'] - p_b = self.options['specific_power'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] - - self.add_output('heat_out', units='W', desc='Waste heat out', shape=(nn,)) - self.add_output('component_cost', units='USD', desc='Battery cost') - self.add_output('component_sizing_margin', - desc='Load fraction of capable power', shape=(nn,)) - self.add_output('max_energy', units='W*h') - - self.declare_partials('heat_out', 'elec_load', val=(1 - eta_b) * np.ones(nn), - rows=range(nn), cols=range(nn)) - self.declare_partials('component_cost', 'battery_weight', val=cost_inc) - self.declare_partials('component_sizing_margin', 'battery_weight') - self.declare_partials('component_sizing_margin', 'elec_load', - rows=range(nn), cols=range(nn)) - self.declare_partials('max_energy', ['battery_weight','specific_energy']) + nn = self.options["num_nodes"] + self.add_input("battery_weight", units="kg", desc="Total battery pack weight") + self.add_input("elec_load", units="W", desc="Electrical load drawn", shape=(nn,)) + e_b = self.options["specific_energy"] + self.add_input("specific_energy", units="W * h / kg", val=e_b) + eta_b = self.options["efficiency"] + p_b = self.options["specific_power"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] + + self.add_output("heat_out", units="W", desc="Waste heat out", shape=(nn,)) + self.add_output("component_cost", units="USD", desc="Battery cost") + self.add_output("component_sizing_margin", desc="Load fraction of capable power", shape=(nn,)) + self.add_output("max_energy", units="W*h") + + self.declare_partials("heat_out", "elec_load", val=(1 - eta_b) * np.ones(nn), rows=range(nn), cols=range(nn)) + self.declare_partials("component_cost", "battery_weight", val=cost_inc) + self.declare_partials("component_sizing_margin", "battery_weight") + self.declare_partials("component_sizing_margin", "elec_load", rows=range(nn), cols=range(nn)) + self.declare_partials("max_energy", ["battery_weight", "specific_energy"]) def compute(self, inputs, outputs): - eta_b = self.options['efficiency'] - p_b = self.options['specific_power'] - e_b = inputs['specific_energy'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] + eta_b = self.options["efficiency"] + p_b = self.options["specific_power"] + e_b = inputs["specific_energy"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] - outputs['heat_out'] = inputs['elec_load'] * (1 - eta_b) - outputs['component_cost'] = inputs['battery_weight'] * cost_inc + cost_base - outputs['component_sizing_margin'] = inputs['elec_load'] / (p_b * inputs['battery_weight']) - outputs['max_energy'] = inputs['battery_weight'] * e_b + outputs["heat_out"] = inputs["elec_load"] * (1 - eta_b) + outputs["component_cost"] = inputs["battery_weight"] * cost_inc + cost_base + outputs["component_sizing_margin"] = inputs["elec_load"] / (p_b * inputs["battery_weight"]) + outputs["max_energy"] = inputs["battery_weight"] * e_b def compute_partials(self, inputs, J): - eta_b = self.options['efficiency'] - p_b = self.options['specific_power'] - e_b = inputs['specific_energy'] - J['component_sizing_margin', 'elec_load'] = 1 / (p_b * inputs['battery_weight']) - J['component_sizing_margin', 'battery_weight'] = - (inputs['elec_load'] / - (p_b * inputs['battery_weight'] ** 2)) - J['max_energy','battery_weight'] = e_b - J['max_energy', 'specific_energy'] = inputs['battery_weight'] \ No newline at end of file + eta_b = self.options["efficiency"] + p_b = self.options["specific_power"] + e_b = inputs["specific_energy"] + J["component_sizing_margin", "elec_load"] = 1 / (p_b * inputs["battery_weight"]) + J["component_sizing_margin", "battery_weight"] = -(inputs["elec_load"] / (p_b * inputs["battery_weight"] ** 2)) + J["max_energy", "battery_weight"] = e_b + J["max_energy", "specific_energy"] = inputs["battery_weight"] diff --git a/openconcept/energy_storage/tests/test_battery.py b/openconcept/energy_storage/tests/test_battery.py index 95e3ffbb..7cea6310 100644 --- a/openconcept/energy_storage/tests/test_battery.py +++ b/openconcept/energy_storage/tests/test_battery.py @@ -9,67 +9,64 @@ class BatteryTestGroup(Group): """ Test the battery component """ + def initialize(self): - self.options.declare('vec_size',default=1,desc="Number of mission analysis points to run") - self.options.declare('efficiency', default=1., desc='Efficiency (dimensionless)') - self.options.declare('p', default=5000., desc='Battery specific power (W/kg)' ) - self.options.declare('e', default=300., desc='Battery spec energy CAREFUL: (Wh/kg)') - self.options.declare('cost_inc', default=50., desc='$ cost per kg') - self.options.declare('cost_base', default=1., desc= '$ cost base') - self.options.declare('use_defaults', default=True) + self.options.declare("vec_size", default=1, desc="Number of mission analysis points to run") + self.options.declare("efficiency", default=1.0, desc="Efficiency (dimensionless)") + self.options.declare("p", default=5000.0, desc="Battery specific power (W/kg)") + self.options.declare("e", default=300.0, desc="Battery spec energy CAREFUL: (Wh/kg)") + self.options.declare("cost_inc", default=50.0, desc="$ cost per kg") + self.options.declare("cost_base", default=1.0, desc="$ cost base") + self.options.declare("use_defaults", default=True) def setup(self): - use_defaults = self.options['use_defaults'] - nn = self.options['vec_size'] + use_defaults = self.options["use_defaults"] + nn = self.options["vec_size"] if not use_defaults: - eta_b = self.options['efficiency'] - p = self.options['p'] - e = self.options['e'] - ci = self.options['cost_inc'] - cb = self.options['cost_base'] - self.add_subsystem('battery', SimpleBattery(num_nodes=nn, - efficiency=eta_b, - specific_power=p, - specific_energy=e, - cost_inc=ci, - cost_base=cb)) + eta_b = self.options["efficiency"] + p = self.options["p"] + e = self.options["e"] + ci = self.options["cost_inc"] + cb = self.options["cost_base"] + self.add_subsystem( + "battery", + SimpleBattery( + num_nodes=nn, efficiency=eta_b, specific_power=p, specific_energy=e, cost_inc=ci, cost_base=cb + ), + ) else: - self.add_subsystem('battery', SimpleBattery(num_nodes=nn)) + self.add_subsystem("battery", SimpleBattery(num_nodes=nn)) - iv = self.add_subsystem('iv', IndepVarComp()) - iv.add_output('battery_weight', val=100, units='kg') - iv.add_output('elec_load', val=np.ones(nn) * 100, units='kW') - self.connect('iv.battery_weight','battery.battery_weight') - self.connect('iv.elec_load','battery.elec_load') + iv = self.add_subsystem("iv", IndepVarComp()) + iv.add_output("battery_weight", val=100, units="kg") + iv.add_output("elec_load", val=np.ones(nn) * 100, units="kW") + self.connect("iv.battery_weight", "battery.battery_weight") + self.connect("iv.elec_load", "battery.elec_load") -class SimpleBatteryTestCase(unittest.TestCase): +class SimpleBatteryTestCase(unittest.TestCase): def test_default_settings(self): prob = Problem(BatteryTestGroup(vec_size=10, use_defaults=True)) - prob.setup(check=True,force_alloc_complex=True) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob['battery.heat_out'], np.ones(10)*100*0.0, tolerance=1e-15) - assert_near_equal(prob['battery.component_sizing_margin'], np.ones(10)*0.20, tolerance=1e-15) - assert_near_equal(prob['battery.component_cost'], 5001, tolerance=1e-15) - assert_near_equal(prob.get_val('battery.max_energy', units='W*h'), 300*100, tolerance=1e-15) + assert_near_equal(prob["battery.heat_out"], np.ones(10) * 100 * 0.0, tolerance=1e-15) + assert_near_equal(prob["battery.component_sizing_margin"], np.ones(10) * 0.20, tolerance=1e-15) + assert_near_equal(prob["battery.component_cost"], 5001, tolerance=1e-15) + assert_near_equal(prob.get_val("battery.max_energy", units="W*h"), 300 * 100, tolerance=1e-15) - partials = prob.check_partials(method='cs',compact_print=True) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_nondefault_settings(self): - prob = Problem(BatteryTestGroup(vec_size=10, - use_defaults=False, - efficiency=0.95, - p=3000, - e=500, - cost_inc=100, - cost_base=0)) - prob.setup(check=True,force_alloc_complex=True) + prob = Problem( + BatteryTestGroup(vec_size=10, use_defaults=False, efficiency=0.95, p=3000, e=500, cost_inc=100, cost_base=0) + ) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob.get_val('battery.heat_out', units='kW'), np.ones(10)*100*0.05, tolerance=1e-15) - assert_near_equal(prob['battery.component_sizing_margin'], np.ones(10)/3, tolerance=1e-15) - assert_near_equal(prob['battery.component_cost'], 10000, tolerance=1e-15) - assert_near_equal(prob.get_val('battery.max_energy', units='W*h'), 500*100, tolerance=1e-15) + assert_near_equal(prob.get_val("battery.heat_out", units="kW"), np.ones(10) * 100 * 0.05, tolerance=1e-15) + assert_near_equal(prob["battery.component_sizing_margin"], np.ones(10) / 3, tolerance=1e-15) + assert_near_equal(prob["battery.component_cost"], 10000, tolerance=1e-15) + assert_near_equal(prob.get_val("battery.max_energy", units="W*h"), 500 * 100, tolerance=1e-15) - partials = prob.check_partials(method='cs',compact_print=True) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) diff --git a/openconcept/examples/B738.py b/openconcept/examples/B738.py index f853e15b..4671f24b 100644 --- a/openconcept/examples/B738.py +++ b/openconcept/examples/B738.py @@ -9,69 +9,73 @@ from openconcept.mission import MissionWithReserve, IntegratorGroup from openconcept.propulsion import CFM56 + class B738AirplaneModel(IntegratorGroup): """ A custom model specific to the Boeing 737-800 airplane. This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] - + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # a propulsion system needs to be defined in order to provide thrust # information for the mission analysis code propulsion_promotes_inputs = ["fltcond|*", "throttle"] - self.add_subsystem('propmodel', CFM56(num_nodes=nn, plot=False), - promotes_inputs=propulsion_promotes_inputs) - - doubler = om.ExecComp(['thrust=2*thrust_in', 'fuel_flow=2*fuel_flow_in'], - thrust_in={'val': 1.0*np.ones((nn,)), - 'units': 'kN'}, - thrust={'val': 1.0*np.ones((nn,)), - 'units': 'kN'}, - fuel_flow={'val': 1.0*np.ones((nn,)), - 'units': 'kg/s', - 'tags': ['integrate', 'state_name:fuel_used', 'state_units:kg', 'state_val:1.0', 'state_promotes:True']}, - fuel_flow_in={'val': 1.0*np.ones((nn,)), - 'units': 'kg/s'}) - - self.add_subsystem('doubler', doubler, promotes_outputs=['*']) - self.connect('propmodel.thrust', 'doubler.thrust_in') - self.connect('propmodel.fuel_flow', 'doubler.fuel_flow_in') + self.add_subsystem("propmodel", CFM56(num_nodes=nn, plot=False), promotes_inputs=propulsion_promotes_inputs) + + doubler = om.ExecComp( + ["thrust=2*thrust_in", "fuel_flow=2*fuel_flow_in"], + thrust_in={"val": 1.0 * np.ones((nn,)), "units": "kN"}, + thrust={"val": 1.0 * np.ones((nn,)), "units": "kN"}, + fuel_flow={ + "val": 1.0 * np.ones((nn,)), + "units": "kg/s", + "tags": ["integrate", "state_name:fuel_used", "state_units:kg", "state_val:1.0", "state_promotes:True"], + }, + fuel_flow_in={"val": 1.0 * np.ones((nn,)), "units": "kg/s"}, + ) + + self.add_subsystem("doubler", doubler, promotes_outputs=["*"]) + self.connect("propmodel.thrust", "doubler.thrust_in") + self.connect("propmodel.fuel_flow", "doubler.fuel_flow_in") # use a different drag coefficient for takeoff versus cruise - if flight_phase not in ['v0v1', 'v1v0', 'v1vr', 'rotate']: - cd0_source = 'ac|aero|polar|CD0_cruise' + if flight_phase not in ["v0v1", "v1v0", "v1vr", "rotate"]: + cd0_source = "ac|aero|polar|CD0_cruise" else: - cd0_source = 'ac|aero|polar|CD0_TO' - self.add_subsystem('drag', PolarDrag(num_nodes=nn), - promotes_inputs=['fltcond|CL', 'ac|geom|*', ('CD0', cd0_source), - 'fltcond|q', ('e', 'ac|aero|polar|e')], - promotes_outputs=['drag']) + cd0_source = "ac|aero|polar|CD0_TO" + self.add_subsystem( + "drag", + PolarDrag(num_nodes=nn), + promotes_inputs=["fltcond|CL", "ac|geom|*", ("CD0", cd0_source), "fltcond|q", ("e", "ac|aero|polar|e")], + promotes_outputs=["drag"], + ) # generally the weights module will be custom to each airplane - passthru = om.ExecComp('OEW=x', - x={'val': 1.0, - 'units': 'kg'}, - OEW={'val': 1.0, - 'units': 'kg'}) - self.add_subsystem('OEW', passthru, - promotes_inputs=[('x', 'ac|weights|OEW')], - promotes_outputs=['OEW']) - - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|weights|MTOW', 'fuel_used'], - units='kg', vec_size=[1, nn], - scaling_factors=[1, -1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) + passthru = om.ExecComp("OEW=x", x={"val": 1.0, "units": "kg"}, OEW={"val": 1.0, "units": "kg"}) + self.add_subsystem("OEW", passthru, promotes_inputs=[("x", "ac|weights|OEW")], promotes_outputs=["OEW"]) + + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|weights|MTOW", "fuel_used"], + units="kg", + vec_size=[1, nn], + scaling_factors=[1, -1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) + class B738AnalysisGroup(om.Group): def setup(self): @@ -79,100 +83,122 @@ def setup(self): nn = 11 # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - dv_comp.add_output_from_dict('ac|weights|OEW') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + dv_comp.add_output_from_dict("ac|weights|OEW") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") # Run a full mission analysis including takeoff, reserve_, cruise,reserve_ and descereserve_nt - analysis = self.add_subsystem('analysis', - MissionWithReserve(num_nodes=nn, - aircraft_model=B738AirplaneModel), - promotes_inputs=['*'], promotes_outputs=['*']) + analysis = self.add_subsystem( + "analysis", + MissionWithReserve(num_nodes=nn, aircraft_model=B738AirplaneModel), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + def configure_problem(): prob = om.Problem() prob.model = B738AnalysisGroup() - prob.model.nonlinear_solver = om.NewtonSolver(iprint=2,solve_subsystems=True) + prob.model.nonlinear_solver = om.NewtonSolver(iprint=2, solve_subsystems=True) prob.model.linear_solver = om.DirectSolver() - prob.model.nonlinear_solver.options['maxiter'] = 20 - prob.model.nonlinear_solver.options['atol'] = 1e-6 - prob.model.nonlinear_solver.options['rtol'] = 1e-6 - prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement='scalar', print_bound_enforce=False) + prob.model.nonlinear_solver.options["maxiter"] = 20 + prob.model.nonlinear_solver.options["atol"] = 1e-6 + prob.model.nonlinear_solver.options["rtol"] = 1e-6 + prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement="scalar", print_bound_enforce=False) return prob + def set_values(prob, num_nodes): # set some (required) mission parameters. Each pahse needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('climb.fltcond|vs', np.linspace(2300., 600.,num_nodes), units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.linspace(230, 220,num_nodes), units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,)) * 4., units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.linspace(265, 258, num_nodes), units='kn') - prob.set_val('descent.fltcond|vs', np.linspace(-1000, -150, num_nodes), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,)) * 250, units='kn') - prob.set_val('reserve_climb.fltcond|vs', np.linspace(3000., 2300.,num_nodes), units='ft/min') - prob.set_val('reserve_climb.fltcond|Ueas', np.linspace(230, 230,num_nodes), units='kn') - prob.set_val('reserve_cruise.fltcond|vs', np.ones((num_nodes,)) * 4., units='ft/min') - prob.set_val('reserve_cruise.fltcond|Ueas', np.linspace(250, 250, num_nodes), units='kn') - prob.set_val('reserve_descent.fltcond|vs', np.linspace(-800, -800, num_nodes), units='ft/min') - prob.set_val('reserve_descent.fltcond|Ueas', np.ones((num_nodes,)) * 250, units='kn') - prob.set_val('loiter.fltcond|vs', np.linspace(0.0, 0.0, num_nodes), units='ft/min') - prob.set_val('loiter.fltcond|Ueas', np.ones((num_nodes,)) * 200, units='kn') - prob.set_val('cruise|h0',33000.,units='ft') - prob.set_val('reserve|h0',15000.,units='ft') - prob.set_val('mission_range',2050,units='NM') + prob.set_val("climb.fltcond|vs", np.linspace(2300.0, 600.0, num_nodes), units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.linspace(230, 220, num_nodes), units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 4.0, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.linspace(265, 258, num_nodes), units="kn") + prob.set_val("descent.fltcond|vs", np.linspace(-1000, -150, num_nodes), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 250, units="kn") + prob.set_val("reserve_climb.fltcond|vs", np.linspace(3000.0, 2300.0, num_nodes), units="ft/min") + prob.set_val("reserve_climb.fltcond|Ueas", np.linspace(230, 230, num_nodes), units="kn") + prob.set_val("reserve_cruise.fltcond|vs", np.ones((num_nodes,)) * 4.0, units="ft/min") + prob.set_val("reserve_cruise.fltcond|Ueas", np.linspace(250, 250, num_nodes), units="kn") + prob.set_val("reserve_descent.fltcond|vs", np.linspace(-800, -800, num_nodes), units="ft/min") + prob.set_val("reserve_descent.fltcond|Ueas", np.ones((num_nodes,)) * 250, units="kn") + prob.set_val("loiter.fltcond|vs", np.linspace(0.0, 0.0, num_nodes), units="ft/min") + prob.set_val("loiter.fltcond|Ueas", np.ones((num_nodes,)) * 200, units="kn") + prob.set_val("cruise|h0", 33000.0, units="ft") + prob.set_val("reserve|h0", 15000.0, units="ft") + prob.set_val("mission_range", 2050, units="NM") + def show_outputs(prob): # print some outputs - vars_list = ['descent.fuel_used_final','loiter.fuel_used_final'] - units = ['lb','lb'] - nice_print_names = ['Block fuel', 'Total fuel'] + vars_list = ["descent.fuel_used_final", "loiter.fuel_used_final"] + units = ["lb", "lb"] + nice_print_names = ["Block fuel", "Total fuel"] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+units[i]) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + units[i]) # plot some stuff plots = True if plots: - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs','fltcond|M','fltcond|CL'] - y_units = ['ft','kn','lbm',None,'ft/min', None, None] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', 'Vertical speed (ft/min)', 'Mach number', 'CL'] - phases = ['climb', 'cruise', 'descent','reserve_climb','reserve_cruise','reserve_descent','loiter'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='737-800 Mission Profile') + x_var = "range" + x_unit = "NM" + y_vars = ["fltcond|h", "fltcond|Ueas", "fuel_used", "throttle", "fltcond|vs", "fltcond|M", "fltcond|CL"] + y_units = ["ft", "kn", "lbm", None, "ft/min", None, None] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + "Mach number", + "CL", + ] + phases = ["climb", "cruise", "descent", "reserve_climb", "reserve_cruise", "reserve_descent", "loiter"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="737-800 Mission Profile", + ) + def run_738_analysis(plots=False): num_nodes = 11 prob = configure_problem() - prob.setup(check=True, mode='fwd') + prob.setup(check=True, mode="fwd") set_values(prob, num_nodes) prob.run_model() prob.model.list_outputs() @@ -182,4 +208,4 @@ def run_738_analysis(plots=False): if __name__ == "__main__": - run_738_analysis(plots=True) + run_738_analysis(plots=True) diff --git a/openconcept/examples/B738_VLM_drag.py b/openconcept/examples/B738_VLM_drag.py index 7fbcb68b..73464bcd 100644 --- a/openconcept/examples/B738_VLM_drag.py +++ b/openconcept/examples/B738_VLM_drag.py @@ -25,70 +25,83 @@ from openconcept.examples.aircraft_data.B738 import data as acdata from openconcept.propulsion import CFM56 + class B738AirplaneModel(IntegratorGroup): """ A custom model specific to the Boeing 737-800 airplane. This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] - + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # a propulsion system needs to be defined in order to provide thrust # information for the mission analysis code propulsion_promotes_inputs = ["fltcond|*", "throttle"] - self.add_subsystem('propmodel', CFM56(num_nodes=nn, plot=False), - promotes_inputs=propulsion_promotes_inputs) - - doubler = om.ExecComp(['thrust=2*thrust_in', 'fuel_flow=2*fuel_flow_in'], - thrust_in={'val': 1.0*np.ones((nn,)), - 'units': 'kN'}, - thrust={'val': 1.0*np.ones((nn,)), - 'units': 'kN'}, - fuel_flow={'val': 1.0*np.ones((nn,)), - 'units': 'kg/s', - 'tags': ['integrate', 'state_name:fuel_used', 'state_units:kg', 'state_val:1.0', 'state_promotes:True']}, - fuel_flow_in={'val': 1.0*np.ones((nn,)), - 'units': 'kg/s'}) - - self.add_subsystem('doubler', doubler, promotes_outputs=['*']) - self.connect('propmodel.thrust', 'doubler.thrust_in') - self.connect('propmodel.fuel_flow', 'doubler.fuel_flow_in') + self.add_subsystem("propmodel", CFM56(num_nodes=nn, plot=False), promotes_inputs=propulsion_promotes_inputs) + + doubler = om.ExecComp( + ["thrust=2*thrust_in", "fuel_flow=2*fuel_flow_in"], + thrust_in={"val": 1.0 * np.ones((nn,)), "units": "kN"}, + thrust={"val": 1.0 * np.ones((nn,)), "units": "kN"}, + fuel_flow={ + "val": 1.0 * np.ones((nn,)), + "units": "kg/s", + "tags": ["integrate", "state_name:fuel_used", "state_units:kg", "state_val:1.0", "state_promotes:True"], + }, + fuel_flow_in={"val": 1.0 * np.ones((nn,)), "units": "kg/s"}, + ) + + self.add_subsystem("doubler", doubler, promotes_outputs=["*"]) + self.connect("propmodel.thrust", "doubler.thrust_in") + self.connect("propmodel.fuel_flow", "doubler.fuel_flow_in") # use a different drag coefficient for takeoff versus cruise oas_surf_dict = {} # options for OpenAeroStruct - oas_surf_dict['t_over_c'] = acdata['ac']['geom']['wing']['toverc']['value'] - self.add_subsystem('drag', VLMDragPolar(num_nodes=nn, num_x=3, num_y=7, - num_twist=3, surf_options=oas_surf_dict), - promotes_inputs=['fltcond|CL', 'fltcond|M', 'fltcond|h', 'fltcond|q', 'ac|geom|wing|S_ref', - 'ac|geom|wing|AR', 'ac|geom|wing|taper', 'ac|geom|wing|c4sweep', - 'ac|geom|wing|twist', 'ac|aero|CD_nonwing'], - promotes_outputs=['drag']) - self.set_input_defaults('ac|aero|CD_nonwing', 0.0145) # based on matching fuel burn of B738.py example + oas_surf_dict["t_over_c"] = acdata["ac"]["geom"]["wing"]["toverc"]["value"] + self.add_subsystem( + "drag", + VLMDragPolar(num_nodes=nn, num_x=3, num_y=7, num_twist=3, surf_options=oas_surf_dict), + promotes_inputs=[ + "fltcond|CL", + "fltcond|M", + "fltcond|h", + "fltcond|q", + "ac|geom|wing|S_ref", + "ac|geom|wing|AR", + "ac|geom|wing|taper", + "ac|geom|wing|c4sweep", + "ac|geom|wing|twist", + "ac|aero|CD_nonwing", + ], + promotes_outputs=["drag"], + ) + self.set_input_defaults("ac|aero|CD_nonwing", 0.0145) # based on matching fuel burn of B738.py example # generally the weights module will be custom to each airplane - passthru = om.ExecComp('OEW=x', - x={'val': 1.0, - 'units': 'kg'}, - OEW={'val': 1.0, - 'units': 'kg'}) - self.add_subsystem('OEW', passthru, - promotes_inputs=[('x', 'ac|weights|OEW')], - promotes_outputs=['OEW']) - - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|weights|MTOW', 'fuel_used'], - units='kg', vec_size=[1, nn], - scaling_factors=[1, -1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) + passthru = om.ExecComp("OEW=x", x={"val": 1.0, "units": "kg"}, OEW={"val": 1.0, "units": "kg"}) + self.add_subsystem("OEW", passthru, promotes_inputs=[("x", "ac|weights|OEW")], promotes_outputs=["OEW"]) + + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|weights|MTOW", "fuel_used"], + units="kg", + vec_size=[1, nn], + scaling_factors=[1, -1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) + class B738AnalysisGroup(om.Group): def setup(self): @@ -96,124 +109,148 @@ def setup(self): nn = 11 # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - dv_comp.add_output_from_dict('ac|weights|OEW') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + dv_comp.add_output_from_dict("ac|weights|OEW") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") # Run a full mission analysis including takeoff, reserve_, cruise,reserve_ and descereserve_nt - analysis = self.add_subsystem('analysis', - MissionWithReserve(num_nodes=nn, - aircraft_model=B738AirplaneModel), - promotes_inputs=['*'], promotes_outputs=['*']) + analysis = self.add_subsystem( + "analysis", + MissionWithReserve(num_nodes=nn, aircraft_model=B738AirplaneModel), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + def configure_problem(): prob = om.Problem() - prob.model.add_subsystem('analysis', B738AnalysisGroup(), promotes=['*']) - prob.model.nonlinear_solver = om.NewtonSolver(iprint=2,solve_subsystems=True) + prob.model.add_subsystem("analysis", B738AnalysisGroup(), promotes=["*"]) + prob.model.nonlinear_solver = om.NewtonSolver(iprint=2, solve_subsystems=True) prob.model.linear_solver = om.DirectSolver() - prob.model.nonlinear_solver.options['maxiter'] = 10 - prob.model.nonlinear_solver.options['atol'] = 1e-6 - prob.model.nonlinear_solver.options['rtol'] = 1e-6 - prob.model.nonlinear_solver.options['err_on_non_converge'] = True - prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement='scalar', print_bound_enforce=False) + prob.model.nonlinear_solver.options["maxiter"] = 10 + prob.model.nonlinear_solver.options["atol"] = 1e-6 + prob.model.nonlinear_solver.options["rtol"] = 1e-6 + prob.model.nonlinear_solver.options["err_on_non_converge"] = True + prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement="scalar", print_bound_enforce=False) prob.driver = om.ScipyOptimizeDriver() - prob.driver.options['optimizer'] = 'SLSQP' - prob.driver.opt_settings['tol'] = 1e-5 - prob.driver.options['debug_print'] = ['objs', 'desvars', 'nl_cons'] - prob.model.add_design_var('cruise|h0', upper=45e3, units='ft') - prob.model.add_constraint('climb.throttle', lower=0.01, upper=1.05) - prob.model.add_constraint('cruise.throttle', lower=0.01, upper=1.05) - prob.model.add_constraint('descent.throttle', lower=0.01, upper=1.05) + prob.driver.options["optimizer"] = "SLSQP" + prob.driver.opt_settings["tol"] = 1e-5 + prob.driver.options["debug_print"] = ["objs", "desvars", "nl_cons"] + prob.model.add_design_var("cruise|h0", upper=45e3, units="ft") + prob.model.add_constraint("climb.throttle", lower=0.01, upper=1.05) + prob.model.add_constraint("cruise.throttle", lower=0.01, upper=1.05) + prob.model.add_constraint("descent.throttle", lower=0.01, upper=1.05) # Find twist distribution that minimizes fuel burn; lock the twist tip in place # to prevent rigid rotation of the whole wing - prob.model.add_design_var('ac|geom|wing|twist', lower=np.array([0, -10, -10]), - upper=np.array([0, 10, 10]), units='deg') - prob.model.add_objective('descent.fuel_used_final') - + prob.model.add_design_var( + "ac|geom|wing|twist", lower=np.array([0, -10, -10]), upper=np.array([0, 10, 10]), units="deg" + ) + prob.model.add_objective("descent.fuel_used_final") + return prob + def set_values(prob, num_nodes): # set some (required) mission parameters. Each pahse needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('climb.fltcond|vs', np.linspace(2300., 600.,num_nodes), units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.linspace(230, 220,num_nodes), units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,)) * 4., units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.linspace(265, 258, num_nodes), units='kn') - prob.set_val('descent.fltcond|vs', np.linspace(-1000, -150, num_nodes), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,)) * 250, units='kn') - prob.set_val('reserve_climb.fltcond|vs', np.linspace(3000., 2300.,num_nodes), units='ft/min') - prob.set_val('reserve_climb.fltcond|Ueas', np.linspace(230, 230,num_nodes), units='kn') - prob.set_val('reserve_cruise.fltcond|vs', np.ones((num_nodes,)) * 4., units='ft/min') - prob.set_val('reserve_cruise.fltcond|Ueas', np.linspace(250, 250, num_nodes), units='kn') - prob.set_val('reserve_descent.fltcond|vs', np.linspace(-800, -800, num_nodes), units='ft/min') - prob.set_val('reserve_descent.fltcond|Ueas', np.ones((num_nodes,)) * 250, units='kn') - prob.set_val('loiter.fltcond|vs', np.linspace(0.0, 0.0, num_nodes), units='ft/min') - prob.set_val('loiter.fltcond|Ueas', np.ones((num_nodes,)) * 200, units='kn') - prob.set_val('cruise|h0',33000.,units='ft') - prob.set_val('reserve|h0',15000.,units='ft') - prob.set_val('mission_range',2050,units='NM') + prob.set_val("climb.fltcond|vs", np.linspace(2300.0, 600.0, num_nodes), units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.linspace(230, 220, num_nodes), units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 4.0, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.linspace(265, 258, num_nodes), units="kn") + prob.set_val("descent.fltcond|vs", np.linspace(-1000, -150, num_nodes), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 250, units="kn") + prob.set_val("reserve_climb.fltcond|vs", np.linspace(3000.0, 2300.0, num_nodes), units="ft/min") + prob.set_val("reserve_climb.fltcond|Ueas", np.linspace(230, 230, num_nodes), units="kn") + prob.set_val("reserve_cruise.fltcond|vs", np.ones((num_nodes,)) * 4.0, units="ft/min") + prob.set_val("reserve_cruise.fltcond|Ueas", np.linspace(250, 250, num_nodes), units="kn") + prob.set_val("reserve_descent.fltcond|vs", np.linspace(-800, -800, num_nodes), units="ft/min") + prob.set_val("reserve_descent.fltcond|Ueas", np.ones((num_nodes,)) * 250, units="kn") + prob.set_val("loiter.fltcond|vs", np.linspace(0.0, 0.0, num_nodes), units="ft/min") + prob.set_val("loiter.fltcond|Ueas", np.ones((num_nodes,)) * 200, units="kn") + prob.set_val("cruise|h0", 33000.0, units="ft") + prob.set_val("reserve|h0", 15000.0, units="ft") + prob.set_val("mission_range", 2050, units="NM") + def show_outputs(prob, plots=True): # print some outputs - vars_list = ['descent.fuel_used_final','loiter.fuel_used_final'] - units = ['lb','lb'] - nice_print_names = ['Block fuel', 'Total fuel'] + vars_list = ["descent.fuel_used_final", "loiter.fuel_used_final"] + units = ["lb", "lb"] + nice_print_names = ["Block fuel", "Total fuel"] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+units[i]) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + units[i]) # plot some stuff if plots: - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs','fltcond|M','fltcond|CL'] - y_units = ['ft','kn','lbm',None,'ft/min', None, None] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', 'Vertical speed (ft/min)', 'Mach number', 'CL'] - phases = ['climb', 'cruise', 'descent','reserve_climb','reserve_cruise','reserve_descent','loiter'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='737-800 Mission Profile') + x_var = "range" + x_unit = "NM" + y_vars = ["fltcond|h", "fltcond|Ueas", "fuel_used", "throttle", "fltcond|vs", "fltcond|M", "fltcond|CL"] + y_units = ["ft", "kn", "lbm", None, "ft/min", None, None] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + "Mach number", + "CL", + ] + phases = ["climb", "cruise", "descent", "reserve_climb", "reserve_cruise", "reserve_descent", "loiter"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="737-800 Mission Profile", + ) + def run_738_analysis(plots=False): num_nodes = 11 prob = configure_problem() - prob.setup(check=True, mode='fwd') + prob.setup(check=True, mode="fwd") set_values(prob, num_nodes) prob.run_model() show_outputs(prob, plots=plots) return prob + def run_738_optimization(plots=False): num_nodes = 11 prob = configure_problem() - prob.setup(check=True, mode='fwd') + prob.setup(check=True, mode="fwd") set_values(prob, num_nodes) prob.run_driver() if plots: diff --git a/openconcept/examples/B738_aerostructural.py b/openconcept/examples/B738_aerostructural.py index 5b0d0a0b..ce61b4ad 100644 --- a/openconcept/examples/B738_aerostructural.py +++ b/openconcept/examples/B738_aerostructural.py @@ -36,285 +36,377 @@ NUM_SPAR = 3 USE_SURROGATE = True + class B738AirplaneModel(IntegratorGroup): """ A custom model specific to the Boeing 737-800 airplane. This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] - + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # a propulsion system needs to be defined in order to provide thrust # information for the mission analysis code propulsion_promotes_inputs = ["fltcond|*", "throttle"] - self.add_subsystem('propmodel', CFM56(num_nodes=nn, plot=False), - promotes_inputs=propulsion_promotes_inputs) - - doubler = om.ExecComp(['thrust=2*thrust_in', 'fuel_flow=2*fuel_flow_in'], - thrust_in={'val': 1.0*np.ones((nn,)), - 'units': 'kN'}, - thrust={'val': 1.0*np.ones((nn,)), - 'units': 'kN'}, - fuel_flow={'val': 1.0*np.ones((nn,)), - 'units': 'kg/s', - 'tags': ['integrate', 'state_name:fuel_used', 'state_units:kg', 'state_val:1.0', 'state_promotes:True']}, - fuel_flow_in={'val': 1.0*np.ones((nn,)), - 'units': 'kg/s'}) - - self.add_subsystem('doubler', doubler, promotes_outputs=['*']) - self.connect('propmodel.thrust', 'doubler.thrust_in') - self.connect('propmodel.fuel_flow', 'doubler.fuel_flow_in') + self.add_subsystem("propmodel", CFM56(num_nodes=nn, plot=False), promotes_inputs=propulsion_promotes_inputs) + + doubler = om.ExecComp( + ["thrust=2*thrust_in", "fuel_flow=2*fuel_flow_in"], + thrust_in={"val": 1.0 * np.ones((nn,)), "units": "kN"}, + thrust={"val": 1.0 * np.ones((nn,)), "units": "kN"}, + fuel_flow={ + "val": 1.0 * np.ones((nn,)), + "units": "kg/s", + "tags": ["integrate", "state_name:fuel_used", "state_units:kg", "state_val:1.0", "state_promotes:True"], + }, + fuel_flow_in={"val": 1.0 * np.ones((nn,)), "units": "kg/s"}, + ) + + self.add_subsystem("doubler", doubler, promotes_outputs=["*"]) + self.connect("propmodel.thrust", "doubler.thrust_in") + self.connect("propmodel.fuel_flow", "doubler.fuel_flow_in") oas_surf_dict = {} # options for OpenAeroStruct # Grid size and number of spline control points (must be same as B738AnalysisGroup) global NUM_X, NUM_Y, NUM_TWIST, NUM_TOVERC, NUM_SKIN, NUM_SPAR, USE_SURROGATE if USE_SURROGATE: - self.add_subsystem('drag', AerostructDragPolar(num_nodes=nn, num_x=NUM_X, num_y=NUM_Y, - num_twist=NUM_TWIST, num_toverc=NUM_TOVERC, - num_skin=NUM_SKIN, num_spar=NUM_SPAR, - surf_options=oas_surf_dict), - promotes_inputs=['fltcond|CL', 'fltcond|M', 'fltcond|h', 'fltcond|q', 'ac|geom|wing|S_ref', - 'ac|geom|wing|AR', 'ac|geom|wing|taper', 'ac|geom|wing|c4sweep', - 'ac|geom|wing|twist', 'ac|geom|wing|toverc', - 'ac|geom|wing|skin_thickness', 'ac|geom|wing|spar_thickness', - 'ac|aero|CD_nonwing'], - promotes_outputs=['drag', 'ac|weights|W_wing', ('failure', 'ac|struct|failure')]) + self.add_subsystem( + "drag", + AerostructDragPolar( + num_nodes=nn, + num_x=NUM_X, + num_y=NUM_Y, + num_twist=NUM_TWIST, + num_toverc=NUM_TOVERC, + num_skin=NUM_SKIN, + num_spar=NUM_SPAR, + surf_options=oas_surf_dict, + ), + promotes_inputs=[ + "fltcond|CL", + "fltcond|M", + "fltcond|h", + "fltcond|q", + "ac|geom|wing|S_ref", + "ac|geom|wing|AR", + "ac|geom|wing|taper", + "ac|geom|wing|c4sweep", + "ac|geom|wing|twist", + "ac|geom|wing|toverc", + "ac|geom|wing|skin_thickness", + "ac|geom|wing|spar_thickness", + "ac|aero|CD_nonwing", + ], + promotes_outputs=["drag", "ac|weights|W_wing", ("failure", "ac|struct|failure")], + ) else: - self.add_subsystem('drag', AerostructDragPolarExact(num_nodes=nn, num_x=NUM_X, num_y=NUM_Y, - num_twist=NUM_TWIST, num_toverc=NUM_TOVERC, - num_skin=NUM_SKIN, num_spar=NUM_SPAR, - surf_options=oas_surf_dict), - promotes_inputs=['fltcond|CL', 'fltcond|M', 'fltcond|h', 'fltcond|q', 'ac|geom|wing|S_ref', - 'ac|geom|wing|AR', 'ac|geom|wing|taper', 'ac|geom|wing|c4sweep', - 'ac|geom|wing|twist', 'ac|geom|wing|toverc', - 'ac|geom|wing|skin_thickness', 'ac|geom|wing|spar_thickness', - 'ac|aero|CD_nonwing'], - promotes_outputs=['drag', 'ac|weights|W_wing', ('failure', 'ac|struct|failure')]) + self.add_subsystem( + "drag", + AerostructDragPolarExact( + num_nodes=nn, + num_x=NUM_X, + num_y=NUM_Y, + num_twist=NUM_TWIST, + num_toverc=NUM_TOVERC, + num_skin=NUM_SKIN, + num_spar=NUM_SPAR, + surf_options=oas_surf_dict, + ), + promotes_inputs=[ + "fltcond|CL", + "fltcond|M", + "fltcond|h", + "fltcond|q", + "ac|geom|wing|S_ref", + "ac|geom|wing|AR", + "ac|geom|wing|taper", + "ac|geom|wing|c4sweep", + "ac|geom|wing|twist", + "ac|geom|wing|toverc", + "ac|geom|wing|skin_thickness", + "ac|geom|wing|spar_thickness", + "ac|aero|CD_nonwing", + ], + promotes_outputs=["drag", "ac|weights|W_wing", ("failure", "ac|struct|failure")], + ) # generally the weights module will be custom to each airplane - passthru = om.ExecComp('OEW=x', - x={'val': 1.0, - 'units': 'kg'}, - OEW={'val': 1.0, - 'units': 'kg'}) - self.add_subsystem('OEW', passthru, - promotes_inputs=[('x', 'ac|weights|OEW')], - promotes_outputs=['OEW']) + passthru = om.ExecComp("OEW=x", x={"val": 1.0, "units": "kg"}, OEW={"val": 1.0, "units": "kg"}) + self.add_subsystem("OEW", passthru, promotes_inputs=[("x", "ac|weights|OEW")], promotes_outputs=["OEW"]) # Use Raymer as estimate for 737 original wing weight, subtract it # out, then add in OpenAeroStruct wing weight estimate - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|weights|MTOW', 'fuel_used', - 'ac|weights|orig_W_wing', - 'ac|weights|W_wing'], - units='kg', vec_size=[1, nn, 1, 1], - scaling_factors=[1, -1, -1, 1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|weights|MTOW", "fuel_used", "ac|weights|orig_W_wing", "ac|weights|W_wing"], + units="kg", + vec_size=[1, nn, 1, 1], + scaling_factors=[1, -1, -1, 1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) + class B738AnalysisGroup(om.Group): def initialize(self): - self.options.declare('num_nodes', default=11, desc='Number of analysis points per flight segment') - self.options.declare('num_x', default=3, desc='Aerostructural chordwise nodes') - self.options.declare('num_y', default=7, desc='Aerostructural halfspan nodes') - self.options.declare('num_twist', default=3, desc='Number of twist control points') - self.options.declare('num_toverc', default=3, desc='Number of t/c control points') - self.options.declare('num_skin', default=3, desc='Number of skin control points') - self.options.declare('num_spar', default=3, desc='Number of spar control points') - self.options.declare('use_surrogate', default=True, desc='Use surrogate for aerostructural drag ' + - 'polar instead of OpenAeroStruct directly') + self.options.declare("num_nodes", default=11, desc="Number of analysis points per flight segment") + self.options.declare("num_x", default=3, desc="Aerostructural chordwise nodes") + self.options.declare("num_y", default=7, desc="Aerostructural halfspan nodes") + self.options.declare("num_twist", default=3, desc="Number of twist control points") + self.options.declare("num_toverc", default=3, desc="Number of t/c control points") + self.options.declare("num_skin", default=3, desc="Number of skin control points") + self.options.declare("num_spar", default=3, desc="Number of spar control points") + self.options.declare( + "use_surrogate", + default=True, + desc="Use surrogate for aerostructural drag " + "polar instead of OpenAeroStruct directly", + ) def setup(self): # Define number of analysis points to run pers mission segment - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] global NUM_X, NUM_Y, NUM_TWIST, NUM_TOVERC, NUM_SKIN, NUM_SPAR, USE_SURROGATE - NUM_X = self.options['num_x'] - NUM_Y = self.options['num_y'] - NUM_TWIST = self.options['num_twist'] - NUM_TOVERC = self.options['num_toverc'] - NUM_SKIN = self.options['num_skin'] - NUM_SPAR = self.options['num_spar'] - USE_SURROGATE = self.options['use_surrogate'] + NUM_X = self.options["num_x"] + NUM_Y = self.options["num_y"] + NUM_TWIST = self.options["num_twist"] + NUM_TOVERC = self.options["num_toverc"] + NUM_SKIN = self.options["num_skin"] + NUM_SPAR = self.options["num_spar"] + USE_SURROGATE = self.options["use_surrogate"] # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") # dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - dv_comp.add_output_from_dict('ac|weights|OEW') + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + dv_comp.add_output_from_dict("ac|weights|OEW") - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") # Aerostructural design parameters twist = np.linspace(-2, 2, NUM_TWIST) - toverc = acdata['ac']['geom']['wing']['toverc']['value'] * np.ones(NUM_TOVERC) + toverc = acdata["ac"]["geom"]["wing"]["toverc"]["value"] * np.ones(NUM_TOVERC) t_skin = np.linspace(0.005, 0.015, NUM_SKIN) t_spar = np.linspace(0.005, 0.01, NUM_SPAR) - self.set_input_defaults('ac|geom|wing|twist', twist, units='deg') - self.set_input_defaults('ac|geom|wing|toverc', toverc) - self.set_input_defaults('ac|geom|wing|skin_thickness', t_skin, units='m') - self.set_input_defaults('ac|geom|wing|spar_thickness', t_spar, units='m') - self.set_input_defaults('ac|aero|CD_nonwing', 0.0145) # based on matching fuel burn of B738.py example + self.set_input_defaults("ac|geom|wing|twist", twist, units="deg") + self.set_input_defaults("ac|geom|wing|toverc", toverc) + self.set_input_defaults("ac|geom|wing|skin_thickness", t_skin, units="m") + self.set_input_defaults("ac|geom|wing|spar_thickness", t_spar, units="m") + self.set_input_defaults("ac|aero|CD_nonwing", 0.0145) # based on matching fuel burn of B738.py example # Compute Raymer wing weight to know what to subtract from the MTOW before adding the OpenAeroStruct weight W_dg = 174.2e3 # design gross weight, lbs - N_z = 1.5*3. # ultimate load factor (1.5 x limit load factor of 3g) - S_w = 1368. # trapezoidal wing area, ft^2 (from photogrammetry) + N_z = 1.5 * 3.0 # ultimate load factor (1.5 x limit load factor of 3g) + S_w = 1368.0 # trapezoidal wing area, ft^2 (from photogrammetry) A = 9.44 # aspect ratio t_c = 0.12 # root thickness to chord ratio taper = 0.159 # taper ratio - sweep = 25. # wing sweep at 25% MAC + sweep = 25.0 # wing sweep at 25% MAC S_csw = 196.8 # wing-mounted control surface area, ft^2 (from photogrammetry) - W_wing_raymer = 0.0051 * (W_dg * N_z)**0.557 * S_w**0.649 * A**0.5 * \ - (t_c)**(-0.4) * (1 + taper)**0.1 / np.cos(np.deg2rad(sweep)) * S_csw**0.1 - self.set_input_defaults('ac|weights|orig_W_wing', W_wing_raymer, units='lb') + W_wing_raymer = ( + 0.0051 + * (W_dg * N_z) ** 0.557 + * S_w**0.649 + * A**0.5 + * (t_c) ** (-0.4) + * (1 + taper) ** 0.1 + / np.cos(np.deg2rad(sweep)) + * S_csw**0.1 + ) + self.set_input_defaults("ac|weights|orig_W_wing", W_wing_raymer, units="lb") # ======================== Mission analysis ======================== # Run a full mission analysis including takeoff, reserve_, cruise,reserve_ and descereserve_nt - analysis = self.add_subsystem('analysis', - BasicMission(num_nodes=nn, - aircraft_model=B738AirplaneModel), - promotes_inputs=['*'], promotes_outputs=['*']) - + analysis = self.add_subsystem( + "analysis", + BasicMission(num_nodes=nn, aircraft_model=B738AirplaneModel), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + # ======================== Aerostructural sizing at 2.5g ======================== # Add single point aerostructural analysis at 2.5g and MTOW to size the wingbox structure - self.add_subsystem('aerostructural_maneuver', Aerostruct(num_x=NUM_X, num_y=NUM_Y, num_twist=NUM_TWIST, - num_toverc=NUM_TOVERC, num_skin=NUM_SKIN, - num_spar=NUM_SPAR), - promotes_inputs=['ac|geom|wing|S_ref', 'ac|geom|wing|AR', 'ac|geom|wing|taper', - 'ac|geom|wing|c4sweep', 'ac|geom|wing|toverc', - 'ac|geom|wing|skin_thickness', 'ac|geom|wing|spar_thickness', - 'ac|geom|wing|twist', 'load_factor'], - promotes_outputs=[('failure', '2_5g_KS_failure')]) - + self.add_subsystem( + "aerostructural_maneuver", + Aerostruct( + num_x=NUM_X, + num_y=NUM_Y, + num_twist=NUM_TWIST, + num_toverc=NUM_TOVERC, + num_skin=NUM_SKIN, + num_spar=NUM_SPAR, + ), + promotes_inputs=[ + "ac|geom|wing|S_ref", + "ac|geom|wing|AR", + "ac|geom|wing|taper", + "ac|geom|wing|c4sweep", + "ac|geom|wing|toverc", + "ac|geom|wing|skin_thickness", + "ac|geom|wing|spar_thickness", + "ac|geom|wing|twist", + "load_factor", + ], + promotes_outputs=[("failure", "2_5g_KS_failure")], + ) + # Flight condition of 2.5g maneuver load case - self.set_input_defaults('aerostructural_maneuver.fltcond|M', 0.8) - self.set_input_defaults('aerostructural_maneuver.fltcond|h', 20e3, units='ft') - self.set_input_defaults('load_factor', 2.5) # multiplier on weights in structural problem + self.set_input_defaults("aerostructural_maneuver.fltcond|M", 0.8) + self.set_input_defaults("aerostructural_maneuver.fltcond|h", 20e3, units="ft") + self.set_input_defaults("load_factor", 2.5) # multiplier on weights in structural problem # Find angle of attack for 2.5g sizing flight condition such that lift = 2.5 * MTOW - self.add_subsystem('dyn_pressure', DynamicPressureComp(num_nodes=1)) - self.add_subsystem('lift', Lift(num_nodes=1), promotes_inputs=['ac|geom|wing|S_ref']) - self.add_subsystem('kg_to_N', om.ExecComp('lift = load_factor * (MTOW - orig_W_wing + W_wing) * a', - lift={'units': 'N'}, - MTOW={'units': 'kg'}, - orig_W_wing={'units': 'kg', 'val': W_wing_raymer/2.20462}, - W_wing={'units': 'kg'}, - a={'units': 'm/s**2', 'val': 9.807}), - promotes_inputs=['load_factor', ('MTOW', 'ac|weights|MTOW')]) - self.add_subsystem('struct_sizing_AoA', om.BalanceComp('alpha', eq_units='N', lhs_name='MTOW', - rhs_name='lift', units='deg', val=10., - lower=0.)) - self.connect('climb.ac|weights|W_wing', 'kg_to_N.W_wing') - self.connect('kg_to_N.lift', 'struct_sizing_AoA.MTOW') - self.connect('aerostructural_maneuver.density.fltcond|rho', 'dyn_pressure.fltcond|rho') - self.connect('aerostructural_maneuver.airspeed.Utrue', 'dyn_pressure.fltcond|Utrue') - self.connect('dyn_pressure.fltcond|q', 'lift.fltcond|q') - self.connect('aerostructural_maneuver.fltcond|CL', 'lift.fltcond|CL') - self.connect('lift.lift', 'struct_sizing_AoA.lift') - self.connect('struct_sizing_AoA.alpha', 'aerostructural_maneuver.fltcond|alpha') - + self.add_subsystem("dyn_pressure", DynamicPressureComp(num_nodes=1)) + self.add_subsystem("lift", Lift(num_nodes=1), promotes_inputs=["ac|geom|wing|S_ref"]) + self.add_subsystem( + "kg_to_N", + om.ExecComp( + "lift = load_factor * (MTOW - orig_W_wing + W_wing) * a", + lift={"units": "N"}, + MTOW={"units": "kg"}, + orig_W_wing={"units": "kg", "val": W_wing_raymer / 2.20462}, + W_wing={"units": "kg"}, + a={"units": "m/s**2", "val": 9.807}, + ), + promotes_inputs=["load_factor", ("MTOW", "ac|weights|MTOW")], + ) + self.add_subsystem( + "struct_sizing_AoA", + om.BalanceComp("alpha", eq_units="N", lhs_name="MTOW", rhs_name="lift", units="deg", val=10.0, lower=0.0), + ) + self.connect("climb.ac|weights|W_wing", "kg_to_N.W_wing") + self.connect("kg_to_N.lift", "struct_sizing_AoA.MTOW") + self.connect("aerostructural_maneuver.density.fltcond|rho", "dyn_pressure.fltcond|rho") + self.connect("aerostructural_maneuver.airspeed.Utrue", "dyn_pressure.fltcond|Utrue") + self.connect("dyn_pressure.fltcond|q", "lift.fltcond|q") + self.connect("aerostructural_maneuver.fltcond|CL", "lift.fltcond|CL") + self.connect("lift.lift", "struct_sizing_AoA.lift") + self.connect("struct_sizing_AoA.alpha", "aerostructural_maneuver.fltcond|alpha") + def configure_problem(num_nodes): prob = om.Problem() - prob.model.add_subsystem('analysis', B738AnalysisGroup(num_nodes=num_nodes), promotes=['*']) - prob.model.nonlinear_solver = om.NewtonSolver(iprint=2,solve_subsystems=True) + prob.model.add_subsystem("analysis", B738AnalysisGroup(num_nodes=num_nodes), promotes=["*"]) + prob.model.nonlinear_solver = om.NewtonSolver(iprint=2, solve_subsystems=True) prob.model.linear_solver = om.DirectSolver() - prob.model.nonlinear_solver.options['maxiter'] = 10 - prob.model.nonlinear_solver.options['atol'] = 1e-6 - prob.model.nonlinear_solver.options['rtol'] = 1e-6 - prob.model.nonlinear_solver.options['err_on_non_converge'] = True - prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement='scalar', print_bound_enforce=True) + prob.model.nonlinear_solver.options["maxiter"] = 10 + prob.model.nonlinear_solver.options["atol"] = 1e-6 + prob.model.nonlinear_solver.options["rtol"] = 1e-6 + prob.model.nonlinear_solver.options["err_on_non_converge"] = True + prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement="scalar", print_bound_enforce=True) prob.driver = om.ScipyOptimizeDriver() - prob.driver.options['optimizer'] = 'SLSQP' - prob.driver.opt_settings['tol'] = 1e-5 - prob.driver.options['debug_print'] = ['objs', 'desvars', 'nl_cons'] + prob.driver.options["optimizer"] = "SLSQP" + prob.driver.opt_settings["tol"] = 1e-5 + prob.driver.options["debug_print"] = ["objs", "desvars", "nl_cons"] # =========================== Mission design variables/constraints =========================== - prob.model.add_objective('descent.fuel_used_final', scaler=1e-4) # minimize block fuel burn - prob.model.add_constraint('climb.throttle', lower=0.01, upper=1.05) - prob.model.add_constraint('cruise.throttle', lower=0.01, upper=1.05) - prob.model.add_constraint('descent.throttle', lower=0.01, upper=1.05) + prob.model.add_objective("descent.fuel_used_final", scaler=1e-4) # minimize block fuel burn + prob.model.add_constraint("climb.throttle", lower=0.01, upper=1.05) + prob.model.add_constraint("cruise.throttle", lower=0.01, upper=1.05) + prob.model.add_constraint("descent.throttle", lower=0.01, upper=1.05) # =========================== Aerostructural wing design variables/constraints =========================== # Find twist distribution that minimizes fuel burn; lock the twist tip in place # to prevent rigid rotation of the whole wing - prob.model.add_design_var('ac|geom|wing|twist', lower=np.array([0, -10, -10]), - upper=np.array([0, 10, 10]), units='deg') - prob.model.add_design_var('ac|geom|wing|AR', lower=5., upper=10.4) # limit to fit in group III gate - prob.model.add_design_var('ac|geom|wing|c4sweep', lower=0., upper=35.) - prob.model.add_design_var('ac|geom|wing|toverc', lower=np.linspace(.03, 0.1, NUM_TOVERC), upper=0.25) - prob.model.add_design_var("ac|geom|wing|spar_thickness", lower=0.003, upper=0.1, scaler=1e2, units='m') - prob.model.add_design_var("ac|geom|wing|skin_thickness", lower=0.003, upper=0.1, scaler=1e2, units='m') - prob.model.add_design_var('ac|geom|wing|taper', lower=.01, upper=0.35, scaler=1e1) - prob.model.add_constraint('2_5g_KS_failure', upper=0.) - + prob.model.add_design_var( + "ac|geom|wing|twist", lower=np.array([0, -10, -10]), upper=np.array([0, 10, 10]), units="deg" + ) + prob.model.add_design_var("ac|geom|wing|AR", lower=5.0, upper=10.4) # limit to fit in group III gate + prob.model.add_design_var("ac|geom|wing|c4sweep", lower=0.0, upper=35.0) + prob.model.add_design_var("ac|geom|wing|toverc", lower=np.linspace(0.03, 0.1, NUM_TOVERC), upper=0.25) + prob.model.add_design_var("ac|geom|wing|spar_thickness", lower=0.003, upper=0.1, scaler=1e2, units="m") + prob.model.add_design_var("ac|geom|wing|skin_thickness", lower=0.003, upper=0.1, scaler=1e2, units="m") + prob.model.add_design_var("ac|geom|wing|taper", lower=0.01, upper=0.35, scaler=1e1) + prob.model.add_constraint("2_5g_KS_failure", upper=0.0) + return prob + def set_values(prob, num_nodes, range=2050): # set some (required) mission parameters. Each phase needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('cruise|h0',35000.,units='ft') - prob.set_val('mission_range',range,units='NM') - prob.set_val('climb.fltcond|vs', np.linspace(2000., 400.,num_nodes), units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.linspace(220, 200,num_nodes), units='kn') - prob.set_val('cruise.fltcond|vs', np.zeros((num_nodes,)), units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.linspace(250.279, 250.279, num_nodes), units='kn') # M 0.78 @ 35k ft - prob.set_val('descent.fltcond|vs', np.linspace(-2000, -1000, num_nodes), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.linspace(240, 250, num_nodes), units='kn') + prob.set_val("cruise|h0", 35000.0, units="ft") + prob.set_val("mission_range", range, units="NM") + prob.set_val("climb.fltcond|vs", np.linspace(2000.0, 400.0, num_nodes), units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.linspace(220, 200, num_nodes), units="kn") + prob.set_val("cruise.fltcond|vs", np.zeros((num_nodes,)), units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.linspace(250.279, 250.279, num_nodes), units="kn") # M 0.78 @ 35k ft + prob.set_val("descent.fltcond|vs", np.linspace(-2000, -1000, num_nodes), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.linspace(240, 250, num_nodes), units="kn") + def show_outputs(prob, plots=True): # print some outputs - vars_list = ['descent.fuel_used_final'] - units = ['lb','lb'] - nice_print_names = ['Block fuel', 'Total fuel'] + vars_list = ["descent.fuel_used_final"] + units = ["lb", "lb"] + nice_print_names = ["Block fuel", "Total fuel"] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+units[i]) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + units[i]) # plot some stuff if plots: - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs','fltcond|M','fltcond|CL'] - y_units = ['ft','kn','lbm',None,'ft/min', None, None] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', 'Vertical speed (ft/min)', 'Mach number', 'CL'] - phases = ['climb', 'cruise', 'descent'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='737-800 Mission Profile') + x_var = "range" + x_unit = "NM" + y_vars = ["fltcond|h", "fltcond|Ueas", "fuel_used", "throttle", "fltcond|vs", "fltcond|M", "fltcond|CL"] + y_units = ["ft", "kn", "lbm", None, "ft/min", None, None] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + "Mach number", + "CL", + ] + phases = ["climb", "cruise", "descent"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="737-800 Mission Profile", + ) + def run_738_analysis(plots=False): num_nodes = 11 @@ -322,7 +414,7 @@ def run_738_analysis(plots=False): NUM_X = 3 NUM_Y = 7 prob = configure_problem(num_nodes) - prob.setup(check=False, mode='fwd') + prob.setup(check=False, mode="fwd") set_values(prob, num_nodes) prob.run_model() om.n2(prob, show_browser=False) @@ -335,13 +427,14 @@ def run_738_analysis(plots=False): print(f"Descent failure = {prob.get_val('descent.ac|struct|failure')}") return prob + def run_738_optimization(plots=False): num_nodes = 11 global NUM_X, NUM_Y NUM_X = 3 NUM_Y = 7 prob = configure_problem(num_nodes) - prob.setup(check=True, mode='fwd') + prob.setup(check=True, mode="fwd") set_values(prob, num_nodes) prob.run_driver() prob.list_problem_vars(driver_scaling=False) diff --git a/openconcept/examples/Caravan.py b/openconcept/examples/Caravan.py index 1768ccdc..0ea3d534 100644 --- a/openconcept/examples/Caravan.py +++ b/openconcept/examples/Caravan.py @@ -12,110 +12,130 @@ from openconcept.aerodynamics import PolarDrag from openconcept.mission import FullMissionAnalysis + class CaravanAirplaneModel(om.Group): """ A custom model specific to the Cessna Caravan airplane. This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # any control variables other than throttle and braking need to be defined here - controls = self.add_subsystem('controls', om.IndepVarComp(), promotes_outputs=['*']) - controls.add_output('prop1rpm', val=np.ones((nn,)) * 2000, units='rpm') + controls = self.add_subsystem("controls", om.IndepVarComp(), promotes_outputs=["*"]) + controls.add_output("prop1rpm", val=np.ones((nn,)) * 2000, units="rpm") # a propulsion system needs to be defined in order to provide thrust # information for the mission analysis code - propulsion_promotes_outputs = ['fuel_flow', 'thrust'] + propulsion_promotes_outputs = ["fuel_flow", "thrust"] propulsion_promotes_inputs = ["fltcond|*", "ac|propulsion|*", "throttle"] - self.add_subsystem('propmodel', TurbopropPropulsionSystem(num_nodes=nn), - promotes_inputs=propulsion_promotes_inputs, - promotes_outputs=propulsion_promotes_outputs) - self.connect('prop1rpm', 'propmodel.prop1.rpm') + self.add_subsystem( + "propmodel", + TurbopropPropulsionSystem(num_nodes=nn), + promotes_inputs=propulsion_promotes_inputs, + promotes_outputs=propulsion_promotes_outputs, + ) + self.connect("prop1rpm", "propmodel.prop1.rpm") # use a different drag coefficient for takeoff versus cruise - if flight_phase not in ['v0v1', 'v1v0', 'v1vr', 'rotate']: - cd0_source = 'ac|aero|polar|CD0_cruise' + if flight_phase not in ["v0v1", "v1v0", "v1vr", "rotate"]: + cd0_source = "ac|aero|polar|CD0_cruise" else: - cd0_source = 'ac|aero|polar|CD0_TO' - self.add_subsystem('drag', PolarDrag(num_nodes=nn), - promotes_inputs=['fltcond|CL', 'ac|geom|*', ('CD0', cd0_source), - 'fltcond|q', ('e', 'ac|aero|polar|e')], - promotes_outputs=['drag']) + cd0_source = "ac|aero|polar|CD0_TO" + self.add_subsystem( + "drag", + PolarDrag(num_nodes=nn), + promotes_inputs=["fltcond|CL", "ac|geom|*", ("CD0", cd0_source), "fltcond|q", ("e", "ac|aero|polar|e")], + promotes_outputs=["drag"], + ) # generally the weights module will be custom to each airplane - self.add_subsystem('OEW', SingleTurboPropEmptyWeight(), - promotes_inputs=['*', ('P_TO', 'ac|propulsion|engine|rating')], - promotes_outputs=['OEW']) - self.connect('propmodel.prop1.component_weight', 'W_propeller') - self.connect('propmodel.eng1.component_weight', 'W_engine') + self.add_subsystem( + "OEW", + SingleTurboPropEmptyWeight(), + promotes_inputs=["*", ("P_TO", "ac|propulsion|engine|rating")], + promotes_outputs=["OEW"], + ) + self.connect("propmodel.prop1.component_weight", "W_propeller") + self.connect("propmodel.eng1.component_weight", "W_engine") # airplanes which consume fuel will need to integrate # fuel usage across the mission and subtract it from TOW - intfuel = self.add_subsystem('intfuel', Integrator(num_nodes=nn, method='simpson', diff_units='s', - time_setup='duration'), promotes_inputs=['*'], promotes_outputs=['*']) - intfuel.add_integrand('fuel_used', rate_name='fuel_flow', val=1.0, units='kg') - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|weights|MTOW', 'fuel_used'], - units='kg', vec_size=[1, nn], - scaling_factors=[1, -1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) + intfuel = self.add_subsystem( + "intfuel", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + intfuel.add_integrand("fuel_used", rate_name="fuel_flow", val=1.0, units="kg") + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|weights|MTOW", "fuel_used"], + units="kg", + vec_size=[1, nn], + scaling_factors=[1, -1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) class CaravanAnalysisGroup(om.Group): - """This is an example of a balanced field takeoff and three-phase mission analysis. - """ + """This is an example of a balanced field takeoff and three-phase mission analysis.""" + def setup(self): # Define number of analysis points to run pers mission segment nn = 11 # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|fuselage|S_wet') - dv_comp.add_output_from_dict('ac|geom|fuselage|width') - dv_comp.add_output_from_dict('ac|geom|fuselage|length') - dv_comp.add_output_from_dict('ac|geom|fuselage|height') - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - dv_comp.add_output_from_dict('ac|propulsion|propeller|diameter') - - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') - - - analysis = self.add_subsystem('analysis', - FullMissionAnalysis(num_nodes=nn, - aircraft_model=CaravanAirplaneModel), - promotes_inputs=['*'], promotes_outputs=['*']) - + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|fuselage|S_wet") + dv_comp.add_output_from_dict("ac|geom|fuselage|width") + dv_comp.add_output_from_dict("ac|geom|fuselage|length") + dv_comp.add_output_from_dict("ac|geom|fuselage|height") + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + dv_comp.add_output_from_dict("ac|propulsion|propeller|diameter") + + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") + + analysis = self.add_subsystem( + "analysis", + FullMissionAnalysis(num_nodes=nn, aircraft_model=CaravanAirplaneModel), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + def run_caravan_analysis(): # Set up OpenMDAO to analyze the airplane @@ -124,74 +144,98 @@ def run_caravan_analysis(): prob.model = CaravanAnalysisGroup() prob.model.nonlinear_solver = om.NewtonSolver(iprint=2, solve_subsystems=True) prob.model.linear_solver = om.DirectSolver() - prob.model.nonlinear_solver.options['maxiter'] = 20 - prob.model.nonlinear_solver.options['atol'] = 1e-6 - prob.model.nonlinear_solver.options['rtol'] = 1e-6 - prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement='scalar', print_bound_enforce=False) - prob.setup(check=True, mode='fwd') + prob.model.nonlinear_solver.options["maxiter"] = 20 + prob.model.nonlinear_solver.options["atol"] = 1e-6 + prob.model.nonlinear_solver.options["rtol"] = 1e-6 + prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement="scalar", print_bound_enforce=False) + prob.setup(check=True, mode="fwd") # set some (required) mission parameters. Each pahse needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('climb.fltcond|vs', np.ones((num_nodes,)) * 850, units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.ones((num_nodes,)) * 104, units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,)) * 0.01, units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.ones((num_nodes,)) * 129, units='kn') - prob.set_val('descent.fltcond|vs', np.ones((num_nodes,)) * (-400), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,)) * 100, units='kn') + prob.set_val("climb.fltcond|vs", np.ones((num_nodes,)) * 850, units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.ones((num_nodes,)) * 104, units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 0.01, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.ones((num_nodes,)) * 129, units="kn") + prob.set_val("descent.fltcond|vs", np.ones((num_nodes,)) * (-400), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 100, units="kn") - prob.set_val('cruise|h0',18000.,units='ft') - prob.set_val('mission_range',250,units='NM') + prob.set_val("cruise|h0", 18000.0, units="ft") + prob.set_val("mission_range", 250, units="NM") # (optional) guesses for takeoff speeds may help with convergence - prob.set_val('v0v1.fltcond|Utrue',np.ones((num_nodes)) * 40,units='kn') - prob.set_val('v1vr.fltcond|Utrue',np.ones((num_nodes)) * 70,units='kn') - prob.set_val('v1v0.fltcond|Utrue',np.ones((num_nodes)) * 60,units='kn') - prob.set_val('rotate.fltcond|Utrue',np.ones((num_nodes)) * 80,units='kn') + prob.set_val("v0v1.fltcond|Utrue", np.ones((num_nodes)) * 40, units="kn") + prob.set_val("v1vr.fltcond|Utrue", np.ones((num_nodes)) * 70, units="kn") + prob.set_val("v1v0.fltcond|Utrue", np.ones((num_nodes)) * 60, units="kn") + prob.set_val("rotate.fltcond|Utrue", np.ones((num_nodes)) * 80, units="kn") # set some airplane-specific values. - prob['climb.OEW.structural_fudge'] = 1.67 - prob['v0v1.throttle'] = np.ones((num_nodes)) - prob['v1vr.throttle'] = np.ones((num_nodes)) - prob['rotate.throttle'] = np.ones((num_nodes)) + prob["climb.OEW.structural_fudge"] = 1.67 + prob["v0v1.throttle"] = np.ones((num_nodes)) + prob["v1vr.throttle"] = np.ones((num_nodes)) + prob["rotate.throttle"] = np.ones((num_nodes)) prob.run_model() return prob + if __name__ == "__main__": from openconcept.utilities.visualization import plot_trajectory + # run the analysis prob = run_caravan_analysis() # print some outputs - vars_list = ['ac|weights|MTOW','climb.OEW','descent.fuel_used_final','v1vr.range_final'] - units = ['lb','lb','lb','ft'] - nice_print_names = ['MTOW', 'OEW', 'Fuel used', 'TOFL ground roll'] + vars_list = ["ac|weights|MTOW", "climb.OEW", "descent.fuel_used_final", "v1vr.range_final"] + units = ["lb", "lb", "lb", "ft"] + nice_print_names = ["MTOW", "OEW", "Fuel used", "TOFL ground roll"] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+units[i]) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + units[i]) # plot some stuff plots = True if plots: - x_var = 'range' - x_unit = 'ft' - y_vars = ['fltcond|Ueas', 'fltcond|h'] - y_units = ['kn', 'ft'] - x_label = 'Distance (ft)' - y_labels = ['Veas airspeed (knots)', 'Altitude (ft)'] - phases = ['v0v1', 'v1vr', 'rotate', 'v1v0'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, - plot_title='Caravan Takeoff') - - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs'] - y_units = ['ft','kn','lbm',None,'ft/min'] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', 'Vertical speed (ft/min)'] - phases = ['climb', 'cruise', 'descent'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='Caravan Mission Profile') - + x_var = "range" + x_unit = "ft" + y_vars = ["fltcond|Ueas", "fltcond|h"] + y_units = ["kn", "ft"] + x_label = "Distance (ft)" + y_labels = ["Veas airspeed (knots)", "Altitude (ft)"] + phases = ["v0v1", "v1vr", "rotate", "v1v0"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + plot_title="Caravan Takeoff", + ) + + x_var = "range" + x_unit = "NM" + y_vars = ["fltcond|h", "fltcond|Ueas", "fuel_used", "throttle", "fltcond|vs"] + y_units = ["ft", "kn", "lbm", None, "ft/min"] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + ] + phases = ["climb", "cruise", "descent"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="Caravan Mission Profile", + ) diff --git a/openconcept/examples/ElectricSinglewithThermal.py b/openconcept/examples/ElectricSinglewithThermal.py index 401d63c8..e9358051 100644 --- a/openconcept/examples/ElectricSinglewithThermal.py +++ b/openconcept/examples/ElectricSinglewithThermal.py @@ -1,6 +1,6 @@ import numpy as np -from openmdao.api import Problem, Group, ScipyOptimizeDriver, DirectSolver, IndepVarComp, NewtonSolver,BoundsEnforceLS +from openmdao.api import Problem, Group, ScipyOptimizeDriver, DirectSolver, IndepVarComp, NewtonSolver, BoundsEnforceLS from openconcept.utilities import DictIndepVarComp, plot_trajectory, LinearInterpolator # imports for the airplane model itself @@ -10,121 +10,131 @@ from openconcept.mission import FullMissionAnalysis - class ElectricTBM850Model(Group): """ A custom model specific to an electrified TBM 850 airplane This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # any control variables other than throttle and braking need to be defined here - controls = self.add_subsystem('controls', IndepVarComp(), promotes_outputs=['*']) - controls.add_output('prop1rpm', val=np.ones((nn,)) * 2000, units='rpm') + controls = self.add_subsystem("controls", IndepVarComp(), promotes_outputs=["*"]) + controls.add_output("prop1rpm", val=np.ones((nn,)) * 2000, units="rpm") - propulsion_promotes_outputs = ['thrust'] - propulsion_promotes_inputs = ["fltcond|*", "ac|propulsion|*", - "throttle", "ac|weights|*", "duration"] + propulsion_promotes_outputs = ["thrust"] + propulsion_promotes_inputs = ["fltcond|*", "ac|propulsion|*", "throttle", "ac|weights|*", "duration"] - self.add_subsystem('propmodel', AllElectricSinglePropulsionSystemWithThermal_Incompressible(num_nodes=nn), - promotes_inputs=propulsion_promotes_inputs, - promotes_outputs=propulsion_promotes_outputs) - self.connect('prop1rpm', 'propmodel.prop1.rpm') + self.add_subsystem( + "propmodel", + AllElectricSinglePropulsionSystemWithThermal_Incompressible(num_nodes=nn), + promotes_inputs=propulsion_promotes_inputs, + promotes_outputs=propulsion_promotes_outputs, + ) + self.connect("prop1rpm", "propmodel.prop1.rpm") # use a different drag coefficient for takeoff versus cruise - if flight_phase not in ['v0v1', 'v1v0', 'v1vr', 'rotate']: - cd0_source = 'ac|aero|polar|CD0_cruise' + if flight_phase not in ["v0v1", "v1v0", "v1vr", "rotate"]: + cd0_source = "ac|aero|polar|CD0_cruise" else: - cd0_source = 'ac|aero|polar|CD0_TO' - self.add_subsystem('drag', PolarDrag(num_nodes=nn), - promotes_inputs=['fltcond|CL', 'ac|geom|*', ('CD0', cd0_source), - 'fltcond|q', ('e', 'ac|aero|polar|e')], - promotes_outputs=['drag']) - self.add_subsystem('weight',LinearInterpolator(num_nodes=nn, units='kg'), - promotes_inputs=[('start_val','ac|weights|MTOW'), - ('end_val','ac|weights|MTOW')], - promotes_outputs=[('vec','weight')]) + cd0_source = "ac|aero|polar|CD0_TO" + self.add_subsystem( + "drag", + PolarDrag(num_nodes=nn), + promotes_inputs=["fltcond|CL", "ac|geom|*", ("CD0", cd0_source), "fltcond|q", ("e", "ac|aero|polar|e")], + promotes_outputs=["drag"], + ) + self.add_subsystem( + "weight", + LinearInterpolator(num_nodes=nn, units="kg"), + promotes_inputs=[("start_val", "ac|weights|MTOW"), ("end_val", "ac|weights|MTOW")], + promotes_outputs=[("vec", "weight")], + ) + class ElectricTBMAnalysisGroup(Group): - """This is an example of a balanced field takeoff and three-phase mission analysis. - """ + """This is an example of a balanced field takeoff and three-phase mission analysis.""" + def setup(self): nn = 11 - dv_comp = self.add_subsystem('dv_comp',DictIndepVarComp(acdata),promotes_outputs=["*"]) - #eventually replace the following aerodynamic parameters with an analysis module (maybe OpenAeroStruct) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|fuselage|S_wet') - dv_comp.add_output_from_dict('ac|geom|fuselage|width') - dv_comp.add_output_from_dict('ac|geom|fuselage|length') - dv_comp.add_output_from_dict('ac|geom|fuselage|height') - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - dv_comp.add_output_from_dict('ac|propulsion|propeller|diameter') - - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') - dv_comp.add_output('ac|propulsion|motor|rating', val=850, units='hp') - dv_comp.add_output('ac|weights|W_battery', val=2000, units='lb') - - mission_data_comp = self.add_subsystem('mission_data_comp',IndepVarComp(),promotes_outputs=["*"]) + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + # eventually replace the following aerodynamic parameters with an analysis module (maybe OpenAeroStruct) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|fuselage|S_wet") + dv_comp.add_output_from_dict("ac|geom|fuselage|width") + dv_comp.add_output_from_dict("ac|geom|fuselage|length") + dv_comp.add_output_from_dict("ac|geom|fuselage|height") + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + dv_comp.add_output_from_dict("ac|propulsion|propeller|diameter") + + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") + dv_comp.add_output("ac|propulsion|motor|rating", val=850, units="hp") + dv_comp.add_output("ac|weights|W_battery", val=2000, units="lb") + + mission_data_comp = self.add_subsystem("mission_data_comp", IndepVarComp(), promotes_outputs=["*"]) # mission_data_comp.add_output('cruise|h0',val=6000, units='m') # mission_data_comp.add_output('design_range',val=150,units='NM') - mission_data_comp.add_output('T_motor_initial', val=15, units='degC') - mission_data_comp.add_output('T_res_initial', val=15.1, units='degC') + mission_data_comp.add_output("T_motor_initial", val=15, units="degC") + mission_data_comp.add_output("T_res_initial", val=15.1, units="degC") - connect_phases_1 = ['v0v1','v1vr','rotate','climb','cruise','descent'] - connect_states_1 = ['propmodel.batt1.SOC','propmodel.motorheatsink.T','propmodel.reservoir.T'] + connect_phases_1 = ["v0v1", "v1vr", "rotate", "climb", "cruise", "descent"] + connect_states_1 = ["propmodel.batt1.SOC", "propmodel.motorheatsink.T", "propmodel.reservoir.T"] extra_states_tuple_1 = [(connect_state, connect_phases_1) for connect_state in connect_states_1] - analysis = self.add_subsystem('analysis',FullMissionAnalysis(num_nodes=nn, - aircraft_model=ElectricTBM850Model, - transition_method='ode'), - promotes_inputs=['*'],promotes_outputs=['*']) - - self.connect('T_motor_initial','v0v1.propmodel.motorheatsink.T_initial') - self.connect('T_res_initial','v0v1.propmodel.reservoir.T_initial') + analysis = self.add_subsystem( + "analysis", + FullMissionAnalysis(num_nodes=nn, aircraft_model=ElectricTBM850Model, transition_method="ode"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + + self.connect("T_motor_initial", "v0v1.propmodel.motorheatsink.T_initial") + self.connect("T_res_initial", "v0v1.propmodel.reservoir.T_initial") + def configure_problem(): prob = Problem() - prob.model= ElectricTBMAnalysisGroup() + prob.model = ElectricTBMAnalysisGroup() - prob.model.nonlinear_solver=NewtonSolver(iprint=2) - prob.model.options['assembled_jac_type'] = 'csc' + prob.model.nonlinear_solver = NewtonSolver(iprint=2) + prob.model.options["assembled_jac_type"] = "csc" prob.model.linear_solver = DirectSolver(assemble_jac=True) - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.nonlinear_solver.options['maxiter'] = 20 - prob.model.nonlinear_solver.options['atol'] = 1e-8 - prob.model.nonlinear_solver.options['rtol'] = 1e-8 - prob.model.nonlinear_solver.linesearch = BoundsEnforceLS(bound_enforcement='scalar',print_bound_enforce=False) - prob.model.add_design_var('mission_range',lower=100,upper=300,scaler=1e-2) - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_objective('mission_range',scaler=-1.0) + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.nonlinear_solver.options["maxiter"] = 20 + prob.model.nonlinear_solver.options["atol"] = 1e-8 + prob.model.nonlinear_solver.options["rtol"] = 1e-8 + prob.model.nonlinear_solver.linesearch = BoundsEnforceLS(bound_enforcement="scalar", print_bound_enforce=False) + prob.model.add_design_var("mission_range", lower=100, upper=300, scaler=1e-2) + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_objective("mission_range", scaler=-1.0) prob.driver = ScipyOptimizeDriver() return prob @@ -132,74 +142,114 @@ def configure_problem(): def set_values(prob, num_nodes): # set some (required) mission parameters. Each pahse needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('rotate.fltcond|Utrue',np.ones((num_nodes))*80,units='kn') - prob.set_val('rotate.accel_vert',np.ones((num_nodes))*0.1,units='m/s**2') - prob.set_val('climb.fltcond|vs', np.ones((num_nodes,))*1000, units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.ones((num_nodes,))*140, units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,))*0.01, units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.ones((num_nodes,))*140, units='kn') - prob.set_val('descent.fltcond|vs', np.ones((num_nodes,))*(-600), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,))*140, units='kn') - - prob.set_val('cruise|h0',6000,units='m') - prob.set_val('mission_range',150,units='NM') + prob.set_val("rotate.fltcond|Utrue", np.ones((num_nodes)) * 80, units="kn") + prob.set_val("rotate.accel_vert", np.ones((num_nodes)) * 0.1, units="m/s**2") + prob.set_val("climb.fltcond|vs", np.ones((num_nodes,)) * 1000, units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.ones((num_nodes,)) * 140, units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 0.01, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.ones((num_nodes,)) * 140, units="kn") + prob.set_val("descent.fltcond|vs", np.ones((num_nodes,)) * (-600), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 140, units="kn") + + prob.set_val("cruise|h0", 6000, units="m") + prob.set_val("mission_range", 150, units="NM") # set some (optional) guesses for takeoff speeds and (required) mission parameters - prob.set_val('v0v1.fltcond|Utrue',np.ones((num_nodes))*50,units='kn') - prob.set_val('v1vr.fltcond|Utrue',np.ones((num_nodes))*85,units='kn') - prob.set_val('v1v0.fltcond|Utrue',np.ones((num_nodes))*85,units='kn') + prob.set_val("v0v1.fltcond|Utrue", np.ones((num_nodes)) * 50, units="kn") + prob.set_val("v1vr.fltcond|Utrue", np.ones((num_nodes)) * 85, units="kn") + prob.set_val("v1v0.fltcond|Utrue", np.ones((num_nodes)) * 85, units="kn") + def show_outputs(prob): # print some outputs - vars_list = ['ac|weights|MTOW','descent.propmodel.batt1.SOC_final','rotate.range_final'] - units = ['lb', None, 'ft'] - nice_print_names = ['MTOW', 'Final battery state of charge','TOFL (over 35ft obstacle)'] + vars_list = ["ac|weights|MTOW", "descent.propmodel.batt1.SOC_final", "rotate.range_final"] + units = ["lb", None, "ft"] + nice_print_names = ["MTOW", "Final battery state of charge", "TOFL (over 35ft obstacle)"] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+str(units[i])) - - y_variables = ['fltcond|h','fltcond|Ueas','throttle','fltcond|vs','propmodel.batt1.SOC','propmodel.motorheatsink.T','propmodel.reservoir.T_out','propmodel.duct.mdot'] - y_units = ['ft','kn',None,'ft/min',None,'degC','degC','kg/s'] + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + str(units[i])) + + y_variables = [ + "fltcond|h", + "fltcond|Ueas", + "throttle", + "fltcond|vs", + "propmodel.batt1.SOC", + "propmodel.motorheatsink.T", + "propmodel.reservoir.T_out", + "propmodel.duct.mdot", + ] + y_units = ["ft", "kn", None, "ft/min", None, "degC", "degC", "kg/s"] # plot some stuff plots = True if plots: - x_var = 'range' - x_unit = 'ft' - y_vars = ['fltcond|h','fltcond|Ueas','throttle','fltcond|vs', - 'propmodel.batt1.SOC','propmodel.motorheatsink.T', - 'propmodel.reservoir.T_out','propmodel.duct.mdot'] - y_units = ['ft', 'kn', None, 'ft/min', - None, 'degC', 'degC', 'lb/s'] - x_label = 'Distance (ft)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Throttle', 'Vertical speed (ft/min)', - 'Battery SOC', 'Motor temp (C)', 'Reservoir outlet temp (C)', 'Cooling duct mass flow (lb/s)'] - phases = ['v0v1', 'v1vr', 'rotate', 'v1v0'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, - plot_title='Elec Single Takeoff') - - x_var = 'range' - x_unit = 'NM' - x_label = 'Range (nmi)' - phases = ['climb', 'cruise', 'descent'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='Elec Single Mission Profile') + x_var = "range" + x_unit = "ft" + y_vars = [ + "fltcond|h", + "fltcond|Ueas", + "throttle", + "fltcond|vs", + "propmodel.batt1.SOC", + "propmodel.motorheatsink.T", + "propmodel.reservoir.T_out", + "propmodel.duct.mdot", + ] + y_units = ["ft", "kn", None, "ft/min", None, "degC", "degC", "lb/s"] + x_label = "Distance (ft)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Throttle", + "Vertical speed (ft/min)", + "Battery SOC", + "Motor temp (C)", + "Reservoir outlet temp (C)", + "Cooling duct mass flow (lb/s)", + ] + phases = ["v0v1", "v1vr", "rotate", "v1v0"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + plot_title="Elec Single Takeoff", + ) + + x_var = "range" + x_unit = "NM" + x_label = "Range (nmi)" + phases = ["climb", "cruise", "descent"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="Elec Single Mission Profile", + ) + def run_electricsingle_analysis(plots=False): num_nodes = 11 prob = configure_problem() - prob.setup(check=True,mode='fwd') + prob.setup(check=True, mode="fwd") set_values(prob, num_nodes) prob.run_model() if plots: show_outputs(prob) return prob - -if __name__ == "__main__": - run_electricsingle_analysis(plots=True) - \ No newline at end of file +if __name__ == "__main__": + run_electricsingle_analysis(plots=True) diff --git a/openconcept/examples/HybridTwin.py b/openconcept/examples/HybridTwin.py index 5e143371..706f0cc3 100644 --- a/openconcept/examples/HybridTwin.py +++ b/openconcept/examples/HybridTwin.py @@ -14,15 +14,18 @@ from openconcept.examples.aircraft_data.KingAirC90GT import data as acdata from openconcept.utilities import AddSubtractComp, Integrator, DictIndepVarComp, LinearInterpolator, plot_trajectory + class AugmentedFBObjective(ExplicitComponent): def setup(self): - self.add_input('fuel_burn', units='kg') - self.add_input('ac|weights|MTOW', units='kg') - self.add_output('mixed_objective', units='kg') - self.declare_partials(['mixed_objective'], ['fuel_burn'], val=1) - self.declare_partials(['mixed_objective'], ['ac|weights|MTOW'], val=1/100) + self.add_input("fuel_burn", units="kg") + self.add_input("ac|weights|MTOW", units="kg") + self.add_output("mixed_objective", units="kg") + self.declare_partials(["mixed_objective"], ["fuel_burn"], val=1) + self.declare_partials(["mixed_objective"], ["ac|weights|MTOW"], val=1 / 100) + def compute(self, inputs, outputs): - outputs['mixed_objective'] = inputs['fuel_burn'] + inputs['ac|weights|MTOW']/100 + outputs["mixed_objective"] = inputs["fuel_burn"] + inputs["ac|weights|MTOW"] / 100 + class SeriesHybridTwinModel(Group): """ @@ -30,338 +33,451 @@ class SeriesHybridTwinModel(Group): This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # any control variables other than throttle and braking need to be defined here - controls = self.add_subsystem('controls', IndepVarComp(), promotes_outputs=['*']) - controls.add_output('proprpm',val=np.ones((nn,))*2000, units='rpm') + controls = self.add_subsystem("controls", IndepVarComp(), promotes_outputs=["*"]) + controls.add_output("proprpm", val=np.ones((nn,)) * 2000, units="rpm") # assume TO happens on battery backup - if flight_phase in ['climb', 'cruise','descent']: - controls.add_output('hybridization',val=0.0) + if flight_phase in ["climb", "cruise", "descent"]: + controls.add_output("hybridization", val=0.0) else: - controls.add_output('hybridization',val=1.0) - - hybrid_factor = self.add_subsystem('hybrid_factor', LinearInterpolator(num_nodes=nn), - promotes_inputs=[('start_val', 'hybridization'), - ('end_val', 'hybridization')]) - - propulsion_promotes_outputs = ['fuel_flow','thrust'] - propulsion_promotes_inputs = ["fltcond|*", "ac|propulsion|*", "throttle", "propulsor_active", - "ac|weights*", 'duration'] - - self.add_subsystem('propmodel', - TwinSeriesHybridElectricPropulsionSystem(num_nodes=nn), - promotes_inputs=propulsion_promotes_inputs, - promotes_outputs=propulsion_promotes_outputs) - self.connect('proprpm', ['propmodel.prop1.rpm', 'propmodel.prop2.rpm']) - self.connect('hybrid_factor.vec', 'propmodel.hybrid_split.power_split_fraction') + controls.add_output("hybridization", val=1.0) + + hybrid_factor = self.add_subsystem( + "hybrid_factor", + LinearInterpolator(num_nodes=nn), + promotes_inputs=[("start_val", "hybridization"), ("end_val", "hybridization")], + ) + + propulsion_promotes_outputs = ["fuel_flow", "thrust"] + propulsion_promotes_inputs = [ + "fltcond|*", + "ac|propulsion|*", + "throttle", + "propulsor_active", + "ac|weights*", + "duration", + ] + + self.add_subsystem( + "propmodel", + TwinSeriesHybridElectricPropulsionSystem(num_nodes=nn), + promotes_inputs=propulsion_promotes_inputs, + promotes_outputs=propulsion_promotes_outputs, + ) + self.connect("proprpm", ["propmodel.prop1.rpm", "propmodel.prop2.rpm"]) + self.connect("hybrid_factor.vec", "propmodel.hybrid_split.power_split_fraction") # use a different drag coefficient for takeoff versus cruise - if flight_phase not in ['v0v1', 'v1v0', 'v1vr', 'rotate']: - cd0_source = 'ac|aero|polar|CD0_cruise' + if flight_phase not in ["v0v1", "v1v0", "v1vr", "rotate"]: + cd0_source = "ac|aero|polar|CD0_cruise" else: - cd0_source = 'ac|aero|polar|CD0_TO' - self.add_subsystem('drag', PolarDrag(num_nodes=nn), - promotes_inputs=['fltcond|CL', 'ac|geom|*', ('CD0', cd0_source), - 'fltcond|q', ('e', 'ac|aero|polar|e')], - promotes_outputs=['drag']) - - self.add_subsystem('OEW',TwinSeriesHybridEmptyWeight(), - promotes_inputs=['*',('P_TO','ac|propulsion|engine|rating')], - promotes_outputs=['OEW']) - self.connect('propmodel.propellers_weight', 'W_propeller') - self.connect('propmodel.eng1.component_weight', 'W_engine') - self.connect('propmodel.gen1.component_weight', 'W_generator') - self.connect('propmodel.motors_weight', 'W_motors') - intfuel = self.add_subsystem('intfuel', Integrator(num_nodes=nn, method='simpson', diff_units='s', - time_setup='duration'), promotes_inputs=['*'], promotes_outputs=['*']) - intfuel.add_integrand('fuel_used', rate_name='fuel_flow', val=1.0, units='kg') - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|weights|MTOW', 'fuel_used'], - units='kg', vec_size=[1, nn], - scaling_factors=[1, -1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) + cd0_source = "ac|aero|polar|CD0_TO" + self.add_subsystem( + "drag", + PolarDrag(num_nodes=nn), + promotes_inputs=["fltcond|CL", "ac|geom|*", ("CD0", cd0_source), "fltcond|q", ("e", "ac|aero|polar|e")], + promotes_outputs=["drag"], + ) + + self.add_subsystem( + "OEW", + TwinSeriesHybridEmptyWeight(), + promotes_inputs=["*", ("P_TO", "ac|propulsion|engine|rating")], + promotes_outputs=["OEW"], + ) + self.connect("propmodel.propellers_weight", "W_propeller") + self.connect("propmodel.eng1.component_weight", "W_engine") + self.connect("propmodel.gen1.component_weight", "W_generator") + self.connect("propmodel.motors_weight", "W_motors") + intfuel = self.add_subsystem( + "intfuel", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + intfuel.add_integrand("fuel_used", rate_name="fuel_flow", val=1.0, units="kg") + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|weights|MTOW", "fuel_used"], + units="kg", + vec_size=[1, nn], + scaling_factors=[1, -1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) + class ElectricTwinAnalysisGroup(Group): - """This is an example of a balanced field takeoff and three-phase mission analysis. - """ + """This is an example of a balanced field takeoff and three-phase mission analysis.""" + def setup(self): # Define number of analysis points to run pers mission segment nn = 11 # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|fuselage|S_wet') - dv_comp.add_output_from_dict('ac|geom|fuselage|width') - dv_comp.add_output_from_dict('ac|geom|fuselage|length') - dv_comp.add_output_from_dict('ac|geom|fuselage|height') - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - dv_comp.add_output_from_dict('ac|weights|W_battery') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - dv_comp.add_output_from_dict('ac|propulsion|propeller|diameter') - dv_comp.add_output_from_dict('ac|propulsion|generator|rating') - dv_comp.add_output_from_dict('ac|propulsion|motor|rating') - dv_comp.add_output('ac|propulsion|battery|specific_energy',val=300,units='W*h/kg') - - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') - dv_comp.add_output_from_dict('ac|num_engines') - - mission_data_comp = self.add_subsystem('mission_data_comp',IndepVarComp(),promotes_outputs=["*"]) - mission_data_comp.add_output('batt_soc_target', val=0.1, units=None) - - analysis = self.add_subsystem('analysis',FullMissionAnalysis(num_nodes=nn, - aircraft_model=SeriesHybridTwinModel), - promotes_inputs=['*'],promotes_outputs=['*']) - - margins = self.add_subsystem('margins',ExecComp('MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload', - MTOW_margin={'units':'lbm','val':100}, - MTOW={'units':'lbm','val':10000}, - OEW={'units':'lbm','val':5000}, - total_fuel={'units':'lbm','val':1000}, - W_battery={'units':'lbm','val':1000}, - payload={'units':'lbm','val':1000}), - promotes_inputs=['payload']) - self.connect('cruise.OEW','margins.OEW') - self.connect('descent.fuel_used_final','margins.total_fuel') - self.connect('ac|weights|MTOW','margins.MTOW') - self.connect('ac|weights|W_battery','margins.W_battery') - - augobj = self.add_subsystem('aug_obj', AugmentedFBObjective(), promotes_outputs=['mixed_objective']) - self.connect('ac|weights|MTOW','aug_obj.ac|weights|MTOW') - self.connect('descent.fuel_used_final','aug_obj.fuel_burn') + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|fuselage|S_wet") + dv_comp.add_output_from_dict("ac|geom|fuselage|width") + dv_comp.add_output_from_dict("ac|geom|fuselage|length") + dv_comp.add_output_from_dict("ac|geom|fuselage|height") + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + dv_comp.add_output_from_dict("ac|weights|W_battery") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + dv_comp.add_output_from_dict("ac|propulsion|propeller|diameter") + dv_comp.add_output_from_dict("ac|propulsion|generator|rating") + dv_comp.add_output_from_dict("ac|propulsion|motor|rating") + dv_comp.add_output("ac|propulsion|battery|specific_energy", val=300, units="W*h/kg") + + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") + dv_comp.add_output_from_dict("ac|num_engines") + + mission_data_comp = self.add_subsystem("mission_data_comp", IndepVarComp(), promotes_outputs=["*"]) + mission_data_comp.add_output("batt_soc_target", val=0.1, units=None) + + analysis = self.add_subsystem( + "analysis", + FullMissionAnalysis(num_nodes=nn, aircraft_model=SeriesHybridTwinModel), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + + margins = self.add_subsystem( + "margins", + ExecComp( + "MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload", + MTOW_margin={"units": "lbm", "val": 100}, + MTOW={"units": "lbm", "val": 10000}, + OEW={"units": "lbm", "val": 5000}, + total_fuel={"units": "lbm", "val": 1000}, + W_battery={"units": "lbm", "val": 1000}, + payload={"units": "lbm", "val": 1000}, + ), + promotes_inputs=["payload"], + ) + self.connect("cruise.OEW", "margins.OEW") + self.connect("descent.fuel_used_final", "margins.total_fuel") + self.connect("ac|weights|MTOW", "margins.MTOW") + self.connect("ac|weights|W_battery", "margins.W_battery") + + augobj = self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) + self.connect("ac|weights|MTOW", "aug_obj.ac|weights|MTOW") + self.connect("descent.fuel_used_final", "aug_obj.fuel_burn") + def configure_problem(): prob = Problem() - prob.model= ElectricTwinAnalysisGroup() - prob.model.nonlinear_solver=NewtonSolver(iprint=1) - prob.model.options['assembled_jac_type'] = 'csc' + prob.model = ElectricTwinAnalysisGroup() + prob.model.nonlinear_solver = NewtonSolver(iprint=1) + prob.model.options["assembled_jac_type"] = "csc" prob.model.linear_solver = DirectSolver(assemble_jac=True) - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.nonlinear_solver.options['maxiter'] = 10 - prob.model.nonlinear_solver.options['atol'] = 1e-7 - prob.model.nonlinear_solver.options['rtol'] = 1e-7 + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.nonlinear_solver.options["maxiter"] = 10 + prob.model.nonlinear_solver.options["atol"] = 1e-7 + prob.model.nonlinear_solver.options["rtol"] = 1e-7 return prob -def set_values(prob, num_nodes, design_range, spec_energy): + +def set_values(prob, num_nodes, design_range, spec_energy): # set some (required) mission parameters. Each pahse needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('climb.fltcond|vs', np.ones((num_nodes,))*1500, units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.ones((num_nodes,))*124, units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,))*0.01, units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.ones((num_nodes,))*170, units='kn') - prob.set_val('descent.fltcond|vs', np.ones((num_nodes,))*(-600), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,))*140, units='kn') - - prob.set_val('cruise|h0',29000,units='ft') - prob.set_val('mission_range',design_range,units='NM') - prob.set_val('payload',1000,units='lb') - prob.set_val('ac|propulsion|battery|specific_energy', spec_energy, units='W*h/kg') + prob.set_val("climb.fltcond|vs", np.ones((num_nodes,)) * 1500, units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.ones((num_nodes,)) * 124, units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 0.01, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.ones((num_nodes,)) * 170, units="kn") + prob.set_val("descent.fltcond|vs", np.ones((num_nodes,)) * (-600), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 140, units="kn") + + prob.set_val("cruise|h0", 29000, units="ft") + prob.set_val("mission_range", design_range, units="NM") + prob.set_val("payload", 1000, units="lb") + prob.set_val("ac|propulsion|battery|specific_energy", spec_energy, units="W*h/kg") # (optional) guesses for takeoff speeds may help with convergence - prob.set_val('v0v1.fltcond|Utrue',np.ones((num_nodes))*50,units='kn') - prob.set_val('v1vr.fltcond|Utrue',np.ones((num_nodes))*85,units='kn') - prob.set_val('v1v0.fltcond|Utrue',np.ones((num_nodes))*85,units='kn') + prob.set_val("v0v1.fltcond|Utrue", np.ones((num_nodes)) * 50, units="kn") + prob.set_val("v1vr.fltcond|Utrue", np.ones((num_nodes)) * 85, units="kn") + prob.set_val("v1v0.fltcond|Utrue", np.ones((num_nodes)) * 85, units="kn") # set some airplane-specific values - prob['analysis.cruise.acmodel.OEW.const.structural_fudge'] = 2.0 - prob['ac|propulsion|propeller|diameter'] = 2.2 - prob['ac|propulsion|engine|rating'] = 1117.2 + prob["analysis.cruise.acmodel.OEW.const.structural_fudge"] = 2.0 + prob["ac|propulsion|propeller|diameter"] = 2.2 + prob["ac|propulsion|engine|rating"] = 1117.2 + def run_hybrid_twin_analysis(plots=False): prob = configure_problem() prob.setup(check=False) - prob['cruise.hybridization'] = 0.05840626452293813 + prob["cruise.hybridization"] = 0.05840626452293813 set_values(prob, 11, 500, 450) prob.run_model() if plots: show_outputs(prob) return prob - + + def show_outputs(prob): # print some outputs - vars_list = ['ac|weights|MTOW','climb.OEW','descent.fuel_used_final', - 'rotate.range_final','descent.propmodel.batt1.SOC_final','cruise.hybridization', - 'ac|weights|W_battery','margins.MTOW_margin', - 'ac|propulsion|motor|rating','ac|propulsion|generator|rating','ac|propulsion|engine|rating', - 'ac|geom|wing|S_ref','v0v1.Vstall_eas','v0v1.takeoff|vr', - 'engineoutclimb.gamma'] - units = ['lb','lb','lb', - 'ft', None, None, - 'lb','lb', - 'hp','hp','hp', - 'ft**2','kn','kn', - 'deg'] - nice_print_names = ['MTOW', 'OEW', 'Fuel used', - 'TOFL (over 35ft obstacle)', 'Final state of charge', 'Cruise hybridization', - 'Battery weight','MTOW margin', - 'Motor rating', 'Generator rating', 'Engine rating', - 'Wing area', 'Stall speed', 'Rotate speed', - 'Engine out climb angle'] + vars_list = [ + "ac|weights|MTOW", + "climb.OEW", + "descent.fuel_used_final", + "rotate.range_final", + "descent.propmodel.batt1.SOC_final", + "cruise.hybridization", + "ac|weights|W_battery", + "margins.MTOW_margin", + "ac|propulsion|motor|rating", + "ac|propulsion|generator|rating", + "ac|propulsion|engine|rating", + "ac|geom|wing|S_ref", + "v0v1.Vstall_eas", + "v0v1.takeoff|vr", + "engineoutclimb.gamma", + ] + units = ["lb", "lb", "lb", "ft", None, None, "lb", "lb", "hp", "hp", "hp", "ft**2", "kn", "kn", "deg"] + nice_print_names = [ + "MTOW", + "OEW", + "Fuel used", + "TOFL (over 35ft obstacle)", + "Final state of charge", + "Cruise hybridization", + "Battery weight", + "MTOW margin", + "Motor rating", + "Generator rating", + "Engine rating", + "Wing area", + "Stall speed", + "Rotate speed", + "Engine out climb angle", + ] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+str(units[i])) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + str(units[i])) # plot some stuff plots = True if plots: - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs','propmodel.batt1.SOC'] - y_units = ['ft','kn','lbm',None,'ft/min',None] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', 'Vertical speed (ft/min)', 'Battery SOC'] - phases = ['v0v1','v1vr','v1v0','rotate'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='Takeoff Profile') - - phases = ['v0v1','v1vr','v1v0','rotate','climb','cruise','descent'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='Full Mission Profile') + x_var = "range" + x_unit = "NM" + y_vars = ["fltcond|h", "fltcond|Ueas", "fuel_used", "throttle", "fltcond|vs", "propmodel.batt1.SOC"] + y_units = ["ft", "kn", "lbm", None, "ft/min", None] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + "Battery SOC", + ] + phases = ["v0v1", "v1vr", "v1v0", "rotate"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="Takeoff Profile", + ) + + phases = ["v0v1", "v1vr", "v1v0", "rotate", "climb", "cruise", "descent"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="Full Mission Profile", + ) + if __name__ == "__main__": # for run type choose choose optimization, comp_sizing, or analysis - run_type = 'example' + run_type = "example" num_nodes = 11 - if run_type == 'example': + if run_type == "example": # runs a default analysis-only mission (no optimization) run_hybrid_twin_analysis(plots=True) else: # can run a sweep of design range and spec energy (not tested) - #design_ranges = [300,350,400,450,500,550,600,650,700] - #specific_energies = [250,300,350,400,450,500,550,600,650,700,750,800] + # design_ranges = [300,350,400,450,500,550,600,650,700] + # specific_energies = [250,300,350,400,450,500,550,600,650,700,750,800] - #or a single point + # or a single point design_ranges = [500] specific_energies = [450] write_logs = False if write_logs: - logging.basicConfig(filename='opt.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s') + logging.basicConfig(filename="opt.log", filemode="w", format="%(name)s - %(levelname)s - %(message)s") # run a sweep of cases at various specific energies and ranges for design_range in design_ranges: for this_spec_energy in specific_energies: try: prob = configure_problem() spec_energy = this_spec_energy - if run_type == 'optimization': - print('======Performing Multidisciplinary Design Optimization===========') - prob.model.add_design_var('ac|weights|MTOW', lower=4000, upper=5700) - prob.model.add_design_var('ac|geom|wing|S_ref',lower=15,upper=40) - prob.model.add_design_var('ac|propulsion|engine|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|motor|rating',lower=450,upper=3000) - prob.model.add_design_var('ac|propulsion|generator|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|weights|W_battery',lower=20,upper=2250) - prob.model.add_design_var('ac|weights|W_fuel_max',lower=500,upper=3000) - prob.model.add_design_var('cruise.hybridization', lower=0.001, upper=0.999) - prob.model.add_design_var('climb.hybridization', lower=0.001, upper=0.999) - prob.model.add_design_var('descent.hybridization', lower=0.01, upper=1.0) - - prob.model.add_constraint('margins.MTOW_margin',lower=0.0) - prob.model.add_constraint('rotate.range_final',upper=1357) - prob.model.add_constraint('v0v1.Vstall_eas',upper=42.0) - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_constraint('climb.throttle',upper=1.05*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('engineoutclimb.gamma',lower=0.02) - prob.model.add_objective('mixed_objective') # TODO add this objective - - elif run_type == 'comp_sizing': - print('======Performing Component Sizing Optimization===========') - prob.model.add_design_var('ac|propulsion|engine|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|motor|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|generator|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|weights|W_battery',lower=20,upper=2250) - prob.model.add_design_var('cruise.hybridization', lower=0.01, upper=0.5) - - prob.model.add_constraint('margins.MTOW_margin',equals=0.0) # TODO implement - prob.model.add_constraint('rotate.range_final',upper=1357) # TODO check units - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_constraint('v0v1.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.throttle',upper=1.05*np.ones(num_nodes)) - prob.model.add_objective('fuel_burn') + if run_type == "optimization": + print("======Performing Multidisciplinary Design Optimization===========") + prob.model.add_design_var("ac|weights|MTOW", lower=4000, upper=5700) + prob.model.add_design_var("ac|geom|wing|S_ref", lower=15, upper=40) + prob.model.add_design_var("ac|propulsion|engine|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|motor|rating", lower=450, upper=3000) + prob.model.add_design_var("ac|propulsion|generator|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|weights|W_battery", lower=20, upper=2250) + prob.model.add_design_var("ac|weights|W_fuel_max", lower=500, upper=3000) + prob.model.add_design_var("cruise.hybridization", lower=0.001, upper=0.999) + prob.model.add_design_var("climb.hybridization", lower=0.001, upper=0.999) + prob.model.add_design_var("descent.hybridization", lower=0.01, upper=1.0) + + prob.model.add_constraint("margins.MTOW_margin", lower=0.0) + prob.model.add_constraint("rotate.range_final", upper=1357) + prob.model.add_constraint("v0v1.Vstall_eas", upper=42.0) + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_constraint("climb.throttle", upper=1.05 * np.ones(num_nodes)) + prob.model.add_constraint( + "climb.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint("engineoutclimb.gamma", lower=0.02) + prob.model.add_objective("mixed_objective") # TODO add this objective + + elif run_type == "comp_sizing": + print("======Performing Component Sizing Optimization===========") + prob.model.add_design_var("ac|propulsion|engine|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|motor|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|generator|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|weights|W_battery", lower=20, upper=2250) + prob.model.add_design_var("cruise.hybridization", lower=0.01, upper=0.5) + + prob.model.add_constraint("margins.MTOW_margin", equals=0.0) # TODO implement + prob.model.add_constraint("rotate.range_final", upper=1357) # TODO check units + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_constraint( + "v0v1.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint("climb.throttle", upper=1.05 * np.ones(num_nodes)) + prob.model.add_objective("fuel_burn") else: - print('======Analyzing Fuel Burn for Given Mision============') - prob.model.add_design_var('cruise.hybridization', lower=0.01, upper=0.5) - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_objective('descent.fuel_used_final') + print("======Analyzing Fuel Burn for Given Mision============") + prob.model.add_design_var("cruise.hybridization", lower=0.01, upper=0.5) + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_objective("descent.fuel_used_final") prob.driver = ScipyOptimizeDriver() if write_logs: - filename_to_save = 'case_'+str(spec_energy)+'_'+str(design_range)+'.sql' + filename_to_save = "case_" + str(spec_energy) + "_" + str(design_range) + ".sql" if os.path.isfile(filename_to_save): - print('Skipping '+filename_to_save) + print("Skipping " + filename_to_save) continue recorder = SqliteRecorder(filename_to_save) prob.driver.add_recorder(recorder) - prob.driver.recording_options['includes'] = [] - prob.driver.recording_options['record_objectives'] = True - prob.driver.recording_options['record_constraints'] = True - prob.driver.recording_options['record_desvars'] = True + prob.driver.recording_options["includes"] = [] + prob.driver.recording_options["record_objectives"] = True + prob.driver.recording_options["record_constraints"] = True + prob.driver.recording_options["record_desvars"] = True prob.setup(check=False) set_values(prob, num_nodes, design_range, spec_energy) run_flag = prob.run_driver() if run_flag: - raise ValueError('Opt failed') + raise ValueError("Opt failed") except BaseException as e: if write_logs: - logging.error('Optimization '+filename_to_save+' failed because '+repr(e)) + logging.error("Optimization " + filename_to_save + " failed because " + repr(e)) prob.cleanup() try: - os.rename(filename_to_save, filename_to_save.split('.sql')[0]+'_failed.sql') + os.rename(filename_to_save, filename_to_save.split(".sql")[0] + "_failed.sql") except WindowsError as we: if write_logs: - logging.error('Error renaming file: '+repr(we)) + logging.error("Error renaming file: " + repr(we)) os.remove(filename_to_save) - show_outputs(prob) \ No newline at end of file + show_outputs(prob) diff --git a/openconcept/examples/HybridTwin_active_thermal.py b/openconcept/examples/HybridTwin_active_thermal.py index ce209c52..9583e9b1 100644 --- a/openconcept/examples/HybridTwin_active_thermal.py +++ b/openconcept/examples/HybridTwin_active_thermal.py @@ -2,7 +2,18 @@ import logging import numpy as np -from openmdao.api import Problem, Group, ScipyOptimizeDriver, ExplicitComponent, ExecComp, SqliteRecorder, DirectSolver, IndepVarComp, NewtonSolver, BoundsEnforceLS +from openmdao.api import ( + Problem, + Group, + ScipyOptimizeDriver, + ExplicitComponent, + ExecComp, + SqliteRecorder, + DirectSolver, + IndepVarComp, + NewtonSolver, + BoundsEnforceLS, +) # imports for the airplane model itself from openconcept.aerodynamics import PolarDrag @@ -10,7 +21,14 @@ from openconcept.propulsion import TwinSeriesHybridElectricThermalPropulsionRefrigerated from openconcept.mission import BasicMission from openconcept.examples.aircraft_data.KingAirC90GT import data as acdata -from openconcept.utilities import AddSubtractComp, MaxComp, Integrator, DictIndepVarComp, LinearInterpolator, plot_trajectory +from openconcept.utilities import ( + AddSubtractComp, + MaxComp, + Integrator, + DictIndepVarComp, + LinearInterpolator, + plot_trajectory, +) """ WARNING: This example has known convergence problems because of the chiller in the @@ -19,15 +37,18 @@ Eytan Adler, 27/10/2021 """ + class AugmentedFBObjective(ExplicitComponent): def setup(self): - self.add_input('fuel_burn', units='kg') - self.add_input('ac|weights|MTOW', units='kg') - self.add_output('mixed_objective', units='kg') - self.declare_partials(['mixed_objective'], ['fuel_burn'], val=1) - self.declare_partials(['mixed_objective'], ['ac|weights|MTOW'], val=1/100) + self.add_input("fuel_burn", units="kg") + self.add_input("ac|weights|MTOW", units="kg") + self.add_output("mixed_objective", units="kg") + self.declare_partials(["mixed_objective"], ["fuel_burn"], val=1) + self.declare_partials(["mixed_objective"], ["ac|weights|MTOW"], val=1 / 100) + def compute(self, inputs, outputs): - outputs['mixed_objective'] = inputs['fuel_burn'] + inputs['ac|weights|MTOW']/100 + outputs["mixed_objective"] = inputs["fuel_burn"] + inputs["ac|weights|MTOW"] / 100 + class SeriesHybridTwinModel(Group): """ @@ -35,271 +56,353 @@ class SeriesHybridTwinModel(Group): thermal management system. This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # any control variables other than throttle and braking need to be defined here - controls = self.add_subsystem('controls', IndepVarComp(), promotes_outputs=['*']) - controls.add_output('proprpm',val=np.ones((nn,))*2000, units='rpm') - controls.add_output('ac|propulsion|thermal|hx|mdot_coolant', val=0.1*np.ones((nn,)), units='kg/s') + controls = self.add_subsystem("controls", IndepVarComp(), promotes_outputs=["*"]) + controls.add_output("proprpm", val=np.ones((nn,)) * 2000, units="rpm") + controls.add_output("ac|propulsion|thermal|hx|mdot_coolant", val=0.1 * np.ones((nn,)), units="kg/s") # assume TO happens on battery backup - if flight_phase in ['climb', 'cruise','descent']: - controls.add_output('hybridization',val=0.0) + if flight_phase in ["climb", "cruise", "descent"]: + controls.add_output("hybridization", val=0.0) else: - controls.add_output('hybridization',val=1.0) - - hybrid_factor = self.add_subsystem('hybrid_factor', LinearInterpolator(num_nodes=nn), - promotes_inputs=[('start_val', 'hybridization'), - ('end_val', 'hybridization')]) - - propulsion_promotes_outputs = ['fuel_flow','thrust'] - propulsion_promotes_inputs = ["fltcond|*", "ac|propulsion|*", "throttle", "propulsor_active", - "ac|weights*", 'duration'] - - self.add_subsystem('propmodel', - TwinSeriesHybridElectricThermalPropulsionRefrigerated(num_nodes=nn), - promotes_inputs=propulsion_promotes_inputs, - promotes_outputs=propulsion_promotes_outputs) - self.connect('proprpm', ['propmodel.prop1.rpm', 'propmodel.prop2.rpm']) - self.connect('hybrid_factor.vec', 'propmodel.hybrid_split.power_split_fraction') + controls.add_output("hybridization", val=1.0) + + hybrid_factor = self.add_subsystem( + "hybrid_factor", + LinearInterpolator(num_nodes=nn), + promotes_inputs=[("start_val", "hybridization"), ("end_val", "hybridization")], + ) + + propulsion_promotes_outputs = ["fuel_flow", "thrust"] + propulsion_promotes_inputs = [ + "fltcond|*", + "ac|propulsion|*", + "throttle", + "propulsor_active", + "ac|weights*", + "duration", + ] + + self.add_subsystem( + "propmodel", + TwinSeriesHybridElectricThermalPropulsionRefrigerated(num_nodes=nn), + promotes_inputs=propulsion_promotes_inputs, + promotes_outputs=propulsion_promotes_outputs, + ) + self.connect("proprpm", ["propmodel.prop1.rpm", "propmodel.prop2.rpm"]) + self.connect("hybrid_factor.vec", "propmodel.hybrid_split.power_split_fraction") # use a different drag coefficient for takeoff versus cruise - if flight_phase not in ['v0v1', 'v1v0', 'v1vr', 'rotate']: - cd0_source = 'ac|aero|polar|CD0_cruise' + if flight_phase not in ["v0v1", "v1v0", "v1vr", "rotate"]: + cd0_source = "ac|aero|polar|CD0_cruise" else: - cd0_source = 'ac|aero|polar|CD0_TO' - self.add_subsystem('drag', PolarDrag(num_nodes=nn), - promotes_inputs=['fltcond|CL', 'ac|geom|*', ('CD0', cd0_source), - 'fltcond|q', ('e', 'ac|aero|polar|e')]) - - self.add_subsystem('OEW',TwinSeriesHybridEmptyWeight(), - promotes_inputs=['*', ('P_TO','ac|propulsion|engine|rating')]) - self.connect('propmodel.propellers_weight', 'W_propeller') - self.connect('propmodel.eng1.component_weight', 'W_engine') - self.connect('propmodel.gen1.component_weight', 'W_generator') - self.connect('propmodel.motors_weight', 'W_motors') + cd0_source = "ac|aero|polar|CD0_TO" + self.add_subsystem( + "drag", + PolarDrag(num_nodes=nn), + promotes_inputs=["fltcond|CL", "ac|geom|*", ("CD0", cd0_source), "fltcond|q", ("e", "ac|aero|polar|e")], + ) + + self.add_subsystem( + "OEW", TwinSeriesHybridEmptyWeight(), promotes_inputs=["*", ("P_TO", "ac|propulsion|engine|rating")] + ) + self.connect("propmodel.propellers_weight", "W_propeller") + self.connect("propmodel.eng1.component_weight", "W_engine") + self.connect("propmodel.gen1.component_weight", "W_generator") + self.connect("propmodel.motors_weight", "W_motors") hxadder = AddSubtractComp() - hxadder.add_equation('OEW',['OEW_orig','W_hx','W_coolant'],scaling_factors=[1,1,1],units='kg') - hxadder.add_equation('drag',['drag_orig','drag_hx'], vec_size=nn, units='N', scaling_factors=[1,1]) - hxadder.add_equation('area_constraint',['hx_frontal_area','nozzle_area'],units='m**2',scaling_factors=[1,-1]) - self.add_subsystem('hxadder',hxadder, promotes_inputs=[('W_coolant','ac|propulsion|thermal|hx|coolant_mass')],promotes_outputs=['OEW','drag']) - self.connect('drag.drag','hxadder.drag_orig') - self.connect('OEW.OEW','hxadder.OEW_orig') - self.connect('propmodel.hx.component_weight','hxadder.W_hx') - self.connect('propmodel.duct.drag','hxadder.drag_hx') - self.connect('propmodel.hx.frontal_area','hxadder.hx_frontal_area') - self.add_subsystem('nozzle_area', MaxComp(num_nodes=nn, units='m**2')) - self.connect('propmodel.area_nozzle', 'nozzle_area.array') - self.connect('nozzle_area.max','hxadder.nozzle_area') - intfuel = self.add_subsystem('intfuel', Integrator(num_nodes=nn, method='simpson', diff_units='s', - time_setup='duration'), promotes_inputs=['*'], promotes_outputs=['*']) - intfuel.add_integrand('fuel_used', rate_name='fuel_flow', val=1.0, units='kg') - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|weights|MTOW', 'fuel_used'], - units='kg', vec_size=[1, nn], - scaling_factors=[1, -1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) + hxadder.add_equation("OEW", ["OEW_orig", "W_hx", "W_coolant"], scaling_factors=[1, 1, 1], units="kg") + hxadder.add_equation("drag", ["drag_orig", "drag_hx"], vec_size=nn, units="N", scaling_factors=[1, 1]) + hxadder.add_equation( + "area_constraint", ["hx_frontal_area", "nozzle_area"], units="m**2", scaling_factors=[1, -1] + ) + self.add_subsystem( + "hxadder", + hxadder, + promotes_inputs=[("W_coolant", "ac|propulsion|thermal|hx|coolant_mass")], + promotes_outputs=["OEW", "drag"], + ) + self.connect("drag.drag", "hxadder.drag_orig") + self.connect("OEW.OEW", "hxadder.OEW_orig") + self.connect("propmodel.hx.component_weight", "hxadder.W_hx") + self.connect("propmodel.duct.drag", "hxadder.drag_hx") + self.connect("propmodel.hx.frontal_area", "hxadder.hx_frontal_area") + self.add_subsystem("nozzle_area", MaxComp(num_nodes=nn, units="m**2")) + self.connect("propmodel.area_nozzle", "nozzle_area.array") + self.connect("nozzle_area.max", "hxadder.nozzle_area") + intfuel = self.add_subsystem( + "intfuel", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + intfuel.add_integrand("fuel_used", rate_name="fuel_flow", val=1.0, units="kg") + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|weights|MTOW", "fuel_used"], + units="kg", + vec_size=[1, nn], + scaling_factors=[1, -1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) + class ElectricTwinAnalysisGroup(Group): - """This is an example of a balanced field takeoff and three-phase mission analysis. - """ + """This is an example of a balanced field takeoff and three-phase mission analysis.""" + def setup(self): # Define number of analysis points to run pers mission segment nn = 11 # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|fuselage|S_wet') - dv_comp.add_output_from_dict('ac|geom|fuselage|width') - dv_comp.add_output_from_dict('ac|geom|fuselage|length') - dv_comp.add_output_from_dict('ac|geom|fuselage|height') - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - dv_comp.add_output_from_dict('ac|weights|W_battery') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - dv_comp.add_output_from_dict('ac|propulsion|propeller|diameter') - dv_comp.add_output_from_dict('ac|propulsion|generator|rating') - dv_comp.add_output_from_dict('ac|propulsion|motor|rating') - - dv_comp.add_output('ac|propulsion|thermal|hx|coolant_mass',val=10.,units='kg') - dv_comp.add_output('ac|propulsion|thermal|hx|channel_width',val=1.,units='mm') - dv_comp.add_output('ac|propulsion|thermal|hx|channel_height',20.,units='mm') - dv_comp.add_output('ac|propulsion|thermal|hx|channel_length',val=0.2,units='m') - dv_comp.add_output('ac|propulsion|thermal|hx|n_parallel',val=50,units=None) - dv_comp.add_output('ac|propulsion|thermal|hx|n_wide_cold',val=430,units=None) - dv_comp.add_output('ac|propulsion|battery|specific_energy',val=300,units='W*h/kg') - - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') - dv_comp.add_output_from_dict('ac|num_engines') - - mission_data_comp = self.add_subsystem('mission_data_comp',IndepVarComp(),promotes_outputs=["*"]) - mission_data_comp.add_output('batt_soc_target', val=0.1, units=None) - mission_data_comp.add_output('T_motor_initial', val=15, units='degC') - mission_data_comp.add_output('T_batt_initial', val=10.1, units='degC') + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|fuselage|S_wet") + dv_comp.add_output_from_dict("ac|geom|fuselage|width") + dv_comp.add_output_from_dict("ac|geom|fuselage|length") + dv_comp.add_output_from_dict("ac|geom|fuselage|height") + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + dv_comp.add_output_from_dict("ac|weights|W_battery") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + dv_comp.add_output_from_dict("ac|propulsion|propeller|diameter") + dv_comp.add_output_from_dict("ac|propulsion|generator|rating") + dv_comp.add_output_from_dict("ac|propulsion|motor|rating") + + dv_comp.add_output("ac|propulsion|thermal|hx|coolant_mass", val=10.0, units="kg") + dv_comp.add_output("ac|propulsion|thermal|hx|channel_width", val=1.0, units="mm") + dv_comp.add_output("ac|propulsion|thermal|hx|channel_height", 20.0, units="mm") + dv_comp.add_output("ac|propulsion|thermal|hx|channel_length", val=0.2, units="m") + dv_comp.add_output("ac|propulsion|thermal|hx|n_parallel", val=50, units=None) + dv_comp.add_output("ac|propulsion|thermal|hx|n_wide_cold", val=430, units=None) + dv_comp.add_output("ac|propulsion|battery|specific_energy", val=300, units="W*h/kg") + + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") + dv_comp.add_output_from_dict("ac|num_engines") + + mission_data_comp = self.add_subsystem("mission_data_comp", IndepVarComp(), promotes_outputs=["*"]) + mission_data_comp.add_output("batt_soc_target", val=0.1, units=None) + mission_data_comp.add_output("T_motor_initial", val=15, units="degC") + mission_data_comp.add_output("T_batt_initial", val=10.1, units="degC") # Ensure that any state variables are connected across the mission as intended - analysis = self.add_subsystem('analysis',BasicMission(num_nodes=nn, - aircraft_model=SeriesHybridTwinModel), - promotes_inputs=['*'],promotes_outputs= - ['*']) - - margins = self.add_subsystem('margins',ExecComp('MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload', - MTOW_margin={'units':'lbm','val':100}, - MTOW={'units':'lbm','val':10000}, - OEW={'units':'lbm','val':5000}, - total_fuel={'units':'lbm','val':1000}, - W_battery={'units':'lbm','val':1000}, - payload={'units':'lbm','val':1000}), - promotes_inputs=['payload']) - self.connect('cruise.OEW','margins.OEW') - self.connect('descent.fuel_used_final','margins.total_fuel') - self.connect('ac|weights|MTOW','margins.MTOW') - self.connect('ac|weights|W_battery','margins.W_battery') - - augobj = self.add_subsystem('aug_obj', AugmentedFBObjective(), promotes_outputs=['mixed_objective']) - self.connect('ac|weights|MTOW','aug_obj.ac|weights|MTOW') - self.connect('descent.fuel_used_final','aug_obj.fuel_burn') - - self.connect('T_motor_initial','climb.propmodel.motorheatsink.T_initial') - self.connect('T_batt_initial','climb.propmodel.batteryheatsink.T_initial') + analysis = self.add_subsystem( + "analysis", + BasicMission(num_nodes=nn, aircraft_model=SeriesHybridTwinModel), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + + margins = self.add_subsystem( + "margins", + ExecComp( + "MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload", + MTOW_margin={"units": "lbm", "val": 100}, + MTOW={"units": "lbm", "val": 10000}, + OEW={"units": "lbm", "val": 5000}, + total_fuel={"units": "lbm", "val": 1000}, + W_battery={"units": "lbm", "val": 1000}, + payload={"units": "lbm", "val": 1000}, + ), + promotes_inputs=["payload"], + ) + self.connect("cruise.OEW", "margins.OEW") + self.connect("descent.fuel_used_final", "margins.total_fuel") + self.connect("ac|weights|MTOW", "margins.MTOW") + self.connect("ac|weights|W_battery", "margins.W_battery") + + augobj = self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) + self.connect("ac|weights|MTOW", "aug_obj.ac|weights|MTOW") + self.connect("descent.fuel_used_final", "aug_obj.fuel_burn") + + self.connect("T_motor_initial", "climb.propmodel.motorheatsink.T_initial") + self.connect("T_batt_initial", "climb.propmodel.batteryheatsink.T_initial") + def configure_problem(): prob = Problem() - prob.model= ElectricTwinAnalysisGroup() - prob.model.nonlinear_solver=NewtonSolver(iprint=2) - prob.model.options['assembled_jac_type'] = 'csc' + prob.model = ElectricTwinAnalysisGroup() + prob.model.nonlinear_solver = NewtonSolver(iprint=2) + prob.model.options["assembled_jac_type"] = "csc" prob.model.linear_solver = DirectSolver(assemble_jac=True) - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.nonlinear_solver.options['maxiter'] = 10 - prob.model.nonlinear_solver.options['atol'] = 1e-8 - prob.model.nonlinear_solver.options['rtol'] = 1e-8 + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.nonlinear_solver.options["maxiter"] = 10 + prob.model.nonlinear_solver.options["atol"] = 1e-8 + prob.model.nonlinear_solver.options["rtol"] = 1e-8 prob.model.nonlinear_solver.linesearch = BoundsEnforceLS() # prob.model.nonlinear_solver.linesearch.options['print_bound_enforce'] = True return prob + def set_values(prob, num_nodes, design_range, spec_energy): # set some (required) mission parameters. Each phase needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('climb.fltcond|vs', np.ones((num_nodes,))*1500, units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.ones((num_nodes,))*124, units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,))*0.01, units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.ones((num_nodes,))*170, units='kn') - prob.set_val('descent.fltcond|vs', np.ones((num_nodes,))*(-600), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,))*140, units='kn') - - prob.set_val('cruise|h0',29000,units='ft') - prob.set_val('mission_range',design_range,units='NM') - prob.set_val('payload',1000,units='lb') - prob.set_val('ac|propulsion|battery|specific_energy', spec_energy, units='W*h/kg') + prob.set_val("climb.fltcond|vs", np.ones((num_nodes,)) * 1500, units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.ones((num_nodes,)) * 124, units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 0.01, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.ones((num_nodes,)) * 170, units="kn") + prob.set_val("descent.fltcond|vs", np.ones((num_nodes,)) * (-600), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 140, units="kn") + + prob.set_val("cruise|h0", 29000, units="ft") + prob.set_val("mission_range", design_range, units="NM") + prob.set_val("payload", 1000, units="lb") + prob.set_val("ac|propulsion|battery|specific_energy", spec_energy, units="W*h/kg") # set some airplane-specific values - prob['analysis.cruise.acmodel.OEW.const.structural_fudge'] = 2.0 - prob['ac|propulsion|propeller|diameter'] = 2.2 - prob['ac|propulsion|engine|rating'] = 1117.2 + prob["analysis.cruise.acmodel.OEW.const.structural_fudge"] = 2.0 + prob["ac|propulsion|propeller|diameter"] = 2.2 + prob["ac|propulsion|engine|rating"] = 1117.2 # Turn off the refrigerator during certain segments - prob['analysis.cruise.acmodel.propmodel.refrig.control.bypass_start'] = 1 - prob['analysis.cruise.acmodel.propmodel.refrig.control.bypass_end'] = 1 + prob["analysis.cruise.acmodel.propmodel.refrig.control.bypass_start"] = 1 + prob["analysis.cruise.acmodel.propmodel.refrig.control.bypass_end"] = 1 # set the initial battery SOC to match the HybridTwin_thermal after takeoff - prob.set_val('climb.propmodel.batt1.SOC_initial', 0.8611499461827815, units=None) + prob.set_val("climb.propmodel.batt1.SOC_initial", 0.8611499461827815, units=None) + def run_hybrid_twin_active_thermal_analysis(plots=False): prob = configure_problem() prob.setup(check=False) - prob['cruise.hybridization'] = 0.05778372636876463 + prob["cruise.hybridization"] = 0.05778372636876463 set_values(prob, 11, 500, 450) prob.run_model() if plots: show_outputs(prob) return prob - + + def show_outputs(prob): # print some outputs - vars_list = ['ac|weights|MTOW','climb.OEW','descent.fuel_used_final', - 'descent.propmodel.batt1.SOC_final','cruise.hybridization', - 'ac|weights|W_battery','margins.MTOW_margin', - 'ac|propulsion|motor|rating','ac|propulsion|generator|rating','ac|propulsion|engine|rating', - 'ac|geom|wing|S_ref', - 'cruise.propmodel.duct.drag', 'ac|propulsion|thermal|hx|coolant_mass', - 'climb.propmodel.duct.mdot'] - units = ['lb','lb','lb', - None, None, - 'lb','lb', - 'hp','hp','hp', - 'ft**2', - 'lbf','lb', - 'lb/s'] - nice_print_names = ['MTOW', 'OEW', 'Fuel used', - 'Final state of charge', 'Cruise hybridization', - 'Battery weight','MTOW margin', - 'Motor rating', 'Generator rating', 'Engine rating', - 'Wing area', - 'Coolant duct cruise drag', 'Coolant mass', - 'Coolant duct mass flow'] + vars_list = [ + "ac|weights|MTOW", + "climb.OEW", + "descent.fuel_used_final", + "descent.propmodel.batt1.SOC_final", + "cruise.hybridization", + "ac|weights|W_battery", + "margins.MTOW_margin", + "ac|propulsion|motor|rating", + "ac|propulsion|generator|rating", + "ac|propulsion|engine|rating", + "ac|geom|wing|S_ref", + "cruise.propmodel.duct.drag", + "ac|propulsion|thermal|hx|coolant_mass", + "climb.propmodel.duct.mdot", + ] + units = ["lb", "lb", "lb", None, None, "lb", "lb", "hp", "hp", "hp", "ft**2", "lbf", "lb", "lb/s"] + nice_print_names = [ + "MTOW", + "OEW", + "Fuel used", + "Final state of charge", + "Cruise hybridization", + "Battery weight", + "MTOW margin", + "Motor rating", + "Generator rating", + "Engine rating", + "Wing area", + "Coolant duct cruise drag", + "Coolant mass", + "Coolant duct mass flow", + ] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+str(units[i])) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + str(units[i])) - print('Motor temps (climb): '+str(prob.get_val('climb.propmodel.motorheatsink.T',units='degC'))) - print('Battery temps (climb): '+str(prob.get_val('climb.propmodel.batteryheatsink.T',units='degC'))) + print("Motor temps (climb): " + str(prob.get_val("climb.propmodel.motorheatsink.T", units="degC"))) + print("Battery temps (climb): " + str(prob.get_val("climb.propmodel.batteryheatsink.T", units="degC"))) # plot some stuff plots = True if plots: - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs','propmodel.batt1.SOC', - 'propmodel.motorheatsink.T','propmodel.batteryheatsink.T'] - y_units = ['ft','kn','lbm',None,'ft/min',None,'degC','degC','inch**2'] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', 'Vertical speed (ft/min)', 'Battery SOC', 'Motor temp', 'Battery temp', 'HX area'] - - phases = ['climb','cruise','descent'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='Full Mission Profile') + x_var = "range" + x_unit = "NM" + y_vars = [ + "fltcond|h", + "fltcond|Ueas", + "fuel_used", + "throttle", + "fltcond|vs", + "propmodel.batt1.SOC", + "propmodel.motorheatsink.T", + "propmodel.batteryheatsink.T", + ] + y_units = ["ft", "kn", "lbm", None, "ft/min", None, "degC", "degC", "inch**2"] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + "Battery SOC", + "Motor temp", + "Battery temp", + "HX area", + ] + + phases = ["climb", "cruise", "descent"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="Full Mission Profile", + ) + if __name__ == "__main__": # for run type choose choose optimization, comp_sizing, or analysis - run_type = 'example' + run_type = "example" num_nodes = 11 - if run_type == 'example': + if run_type == "example": # runs a default analysis-only mission (no optimization) run_hybrid_twin_active_thermal_analysis(plots=True) else: # can run a sweep of design range and spec energy (not tested) - #design_ranges = [300,350,400,450,500,550,600,650,700] - #specific_energies = [250,300,350,400,450,500,550,600,650,700,750,800] + # design_ranges = [300,350,400,450,500,550,600,650,700] + # specific_energies = [250,300,350,400,450,500,550,600,650,700,750,800] # or a single point design_ranges = [500] @@ -307,7 +410,7 @@ def show_outputs(prob): write_logs = False if write_logs: - logging.basicConfig(filename='opt.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s') + logging.basicConfig(filename="opt.log", filemode="w", format="%(name)s - %(levelname)s - %(message)s") last_successful_opt = None # run a sweep of cases at various specific energies and ranges @@ -316,118 +419,152 @@ def show_outputs(prob): try: prob = configure_problem() spec_energy = this_spec_energy - if run_type == 'optimization': - print('======Performing Multidisciplinary Design Optimization===========') - prob.model.add_design_var('ac|weights|MTOW', lower=4000, upper=5700) - prob.model.add_design_var('ac|geom|wing|S_ref',lower=15,upper=40) - prob.model.add_design_var('ac|propulsion|engine|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|motor|rating',lower=450,upper=3000) - prob.model.add_design_var('ac|propulsion|generator|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|weights|W_battery',lower=20,upper=2250) - prob.model.add_design_var('ac|weights|W_fuel_max',lower=500,upper=3000) - prob.model.add_design_var('cruise.hybridization', lower=0.001, upper=0.999) - prob.model.add_design_var('climb.hybridization', lower=0.001, upper=0.999) - prob.model.add_design_var('descent.hybridization', lower=0.01, upper=1.0) - prob.model.add_design_var('ac|propulsion|thermal|duct|area_nozzle', lower=1., upper=200.) - prob.model.add_design_var('ac|propulsion|thermal|hx|n_wide_cold', lower=100., upper=1500.) - prob.model.add_constraint('margins.MTOW_margin',lower=0.0) - prob.model.add_design_var('ac|propulsion|thermal|hx|coolant_mass',lower=5.0,upper=15.0) + if run_type == "optimization": + print("======Performing Multidisciplinary Design Optimization===========") + prob.model.add_design_var("ac|weights|MTOW", lower=4000, upper=5700) + prob.model.add_design_var("ac|geom|wing|S_ref", lower=15, upper=40) + prob.model.add_design_var("ac|propulsion|engine|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|motor|rating", lower=450, upper=3000) + prob.model.add_design_var("ac|propulsion|generator|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|weights|W_battery", lower=20, upper=2250) + prob.model.add_design_var("ac|weights|W_fuel_max", lower=500, upper=3000) + prob.model.add_design_var("cruise.hybridization", lower=0.001, upper=0.999) + prob.model.add_design_var("climb.hybridization", lower=0.001, upper=0.999) + prob.model.add_design_var("descent.hybridization", lower=0.01, upper=1.0) + prob.model.add_design_var("ac|propulsion|thermal|duct|area_nozzle", lower=1.0, upper=200.0) + prob.model.add_design_var("ac|propulsion|thermal|hx|n_wide_cold", lower=100.0, upper=1500.0) + prob.model.add_constraint("margins.MTOW_margin", lower=0.0) + prob.model.add_design_var("ac|propulsion|thermal|hx|coolant_mass", lower=5.0, upper=15.0) # prob.model.add_constraint('design_mission.residuals.fuel_capacity_margin',lower=0.0) - prob.model.add_constraint('rotate.range_final',upper=1357) - prob.model.add_constraint('v0v1.Vstall_eas',upper=42.0) - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_constraint('climb.throttle',upper=1.05*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('engineoutclimb.gamma',lower=0.02) - prob.model.add_constraint('climb.propmodel.motorheatsink.T',upper=363.*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.motorheatsink.T',upper=363.*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.batteryheatsink.T',upper=323.*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.batteryheatsink.T',upper=323.*np.ones(num_nodes)) - prob.model.add_constraint('climb.hxadder.area_constraint',lower=0.001) - prob.model.add_objective('mixed_objective') # TODO add this objective - - elif run_type == 'comp_sizing': - print('======Performing Component Sizing Optimization===========') - prob.model.add_design_var('ac|propulsion|engine|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|motor|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|generator|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|weights|W_battery',lower=20,upper=2250) - prob.model.add_design_var('cruise.hybridization', lower=0.01, upper=0.5) - - prob.model.add_constraint('margins.MTOW_margin',equals=0.0) # TODO implement - prob.model.add_constraint('rotate.range_final',upper=1357) # TODO check units - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_constraint('v0v1.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.throttle',upper=1.05*np.ones(num_nodes)) - prob.model.add_objective('fuel_burn') + prob.model.add_constraint("rotate.range_final", upper=1357) + prob.model.add_constraint("v0v1.Vstall_eas", upper=42.0) + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_constraint("climb.throttle", upper=1.05 * np.ones(num_nodes)) + prob.model.add_constraint( + "climb.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint("engineoutclimb.gamma", lower=0.02) + prob.model.add_constraint("climb.propmodel.motorheatsink.T", upper=363.0 * np.ones(num_nodes)) + prob.model.add_constraint("cruise.propmodel.motorheatsink.T", upper=363.0 * np.ones(num_nodes)) + prob.model.add_constraint("climb.propmodel.batteryheatsink.T", upper=323.0 * np.ones(num_nodes)) + prob.model.add_constraint( + "cruise.propmodel.batteryheatsink.T", upper=323.0 * np.ones(num_nodes) + ) + prob.model.add_constraint("climb.hxadder.area_constraint", lower=0.001) + prob.model.add_objective("mixed_objective") # TODO add this objective + + elif run_type == "comp_sizing": + print("======Performing Component Sizing Optimization===========") + prob.model.add_design_var("ac|propulsion|engine|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|motor|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|generator|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|weights|W_battery", lower=20, upper=2250) + prob.model.add_design_var("cruise.hybridization", lower=0.01, upper=0.5) + + prob.model.add_constraint("margins.MTOW_margin", equals=0.0) # TODO implement + prob.model.add_constraint("rotate.range_final", upper=1357) # TODO check units + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_constraint( + "v0v1.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint("climb.throttle", upper=1.05 * np.ones(num_nodes)) + prob.model.add_objective("fuel_burn") else: - print('======Analyzing Fuel Burn for Given Mision============') - prob.model.add_design_var('cruise.hybridization', lower=0.01, upper=0.5) - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_objective('descent.fuel_used_final') + print("======Analyzing Fuel Burn for Given Mision============") + prob.model.add_design_var("cruise.hybridization", lower=0.01, upper=0.5) + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_objective("descent.fuel_used_final") prob.driver = ScipyOptimizeDriver() if write_logs: - filename_to_save = 'case_'+str(spec_energy)+'_'+str(design_range)+'.sql' + filename_to_save = "case_" + str(spec_energy) + "_" + str(design_range) + ".sql" if os.path.isfile(filename_to_save): if design_range != 300: last_successful_opt = filename_to_save else: - last_successful_opt = 'case_'+str(spec_energy+50)+'_'+str(700)+'.sql' - print('Skipping '+filename_to_save) + last_successful_opt = "case_" + str(spec_energy + 50) + "_" + str(700) + ".sql" + print("Skipping " + filename_to_save) continue recorder = SqliteRecorder(filename_to_save) prob.driver.add_recorder(recorder) - prob.driver.recording_options['includes'] = [] - prob.driver.recording_options['record_objectives'] = True - prob.driver.recording_options['record_constraints'] = True - prob.driver.recording_options['record_desvars'] = True + prob.driver.recording_options["includes"] = [] + prob.driver.recording_options["record_objectives"] = True + prob.driver.recording_options["record_constraints"] = True + prob.driver.recording_options["record_desvars"] = True prob.setup(check=False) set_values(prob, num_nodes, design_range, spec_energy) if last_successful_opt is not None: cr = CaseReader(last_successful_opt) - driver_cases = cr.list_cases('driver') + driver_cases = cr.list_cases("driver") case = cr.get_case(driver_cases[-1]) design_vars = case.get_design_vars() for key in design_vars.keys(): - prob.set_val(key,design_vars[key]) + prob.set_val(key, design_vars[key]) run_flag = prob.run_driver() if run_flag: - raise ValueError('Opt failed') + raise ValueError("Opt failed") else: if write_logs: last_successful_opt = filename_to_save prob.cleanup() except BaseException as e: if write_logs: - logging.error('Optimization '+filename_to_save+' failed because '+repr(e)) + logging.error("Optimization " + filename_to_save + " failed because " + repr(e)) prob.cleanup() try: if write_logs: - os.rename(filename_to_save, filename_to_save.split('.sql')[0]+'_failed.sql') + os.rename(filename_to_save, filename_to_save.split(".sql")[0] + "_failed.sql") except WindowsError as we: if write_logs: - logging.error('Error renaming file: '+repr(we)) + logging.error("Error renaming file: " + repr(we)) os.remove(filename_to_save) - show_outputs(prob) \ No newline at end of file + show_outputs(prob) diff --git a/openconcept/examples/HybridTwin_thermal.py b/openconcept/examples/HybridTwin_thermal.py index 960473ce..5cd7f414 100644 --- a/openconcept/examples/HybridTwin_thermal.py +++ b/openconcept/examples/HybridTwin_thermal.py @@ -2,7 +2,17 @@ import logging import numpy as np -from openmdao.api import Problem, Group, ScipyOptimizeDriver, ExplicitComponent, ExecComp, SqliteRecorder, DirectSolver, IndepVarComp, NewtonSolver +from openmdao.api import ( + Problem, + Group, + ScipyOptimizeDriver, + ExplicitComponent, + ExecComp, + SqliteRecorder, + DirectSolver, + IndepVarComp, + NewtonSolver, +) # imports for the airplane model itself from openconcept.aerodynamics import PolarDrag @@ -10,17 +20,27 @@ from openconcept.propulsion import TwinSeriesHybridElectricThermalPropulsionSystem from openconcept.mission import FullMissionAnalysis from openconcept.examples.aircraft_data.KingAirC90GT import data as acdata -from openconcept.utilities import AddSubtractComp, MaxComp, Integrator, DictIndepVarComp, LinearInterpolator, plot_trajectory +from openconcept.utilities import ( + AddSubtractComp, + MaxComp, + Integrator, + DictIndepVarComp, + LinearInterpolator, + plot_trajectory, +) + class AugmentedFBObjective(ExplicitComponent): def setup(self): - self.add_input('fuel_burn', units='kg') - self.add_input('ac|weights|MTOW', units='kg') - self.add_output('mixed_objective', units='kg') - self.declare_partials(['mixed_objective'], ['fuel_burn'], val=1) - self.declare_partials(['mixed_objective'], ['ac|weights|MTOW'], val=1/100) + self.add_input("fuel_burn", units="kg") + self.add_input("ac|weights|MTOW", units="kg") + self.add_output("mixed_objective", units="kg") + self.declare_partials(["mixed_objective"], ["fuel_burn"], val=1) + self.declare_partials(["mixed_objective"], ["ac|weights|MTOW"], val=1 / 100) + def compute(self, inputs, outputs): - outputs['mixed_objective'] = inputs['fuel_burn'] + inputs['ac|weights|MTOW']/100 + outputs["mixed_objective"] = inputs["fuel_burn"] + inputs["ac|weights|MTOW"] / 100 + class SeriesHybridTwinModel(Group): """ @@ -28,278 +48,394 @@ class SeriesHybridTwinModel(Group): This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # any control variables other than throttle and braking need to be defined here - controls = self.add_subsystem('controls', IndepVarComp(), promotes_outputs=['*']) - controls.add_output('proprpm',val=np.ones((nn,))*2000, units='rpm') - controls.add_output('ac|propulsion|thermal|hx|mdot_coolant', val=0.1*np.ones((nn,)), units='kg/s') + controls = self.add_subsystem("controls", IndepVarComp(), promotes_outputs=["*"]) + controls.add_output("proprpm", val=np.ones((nn,)) * 2000, units="rpm") + controls.add_output("ac|propulsion|thermal|hx|mdot_coolant", val=0.1 * np.ones((nn,)), units="kg/s") # assume TO happens on battery backup - if flight_phase in ['climb', 'cruise','descent']: - controls.add_output('hybridization',val=0.0) + if flight_phase in ["climb", "cruise", "descent"]: + controls.add_output("hybridization", val=0.0) else: - controls.add_output('hybridization',val=1.0) - - hybrid_factor = self.add_subsystem('hybrid_factor', LinearInterpolator(num_nodes=nn), - promotes_inputs=[('start_val', 'hybridization'), - ('end_val', 'hybridization')]) - - propulsion_promotes_outputs = ['fuel_flow','thrust', 'ac|propulsion|thermal|duct|area_nozzle'] - propulsion_promotes_inputs = ["fltcond|*", "ac|propulsion|*", "throttle", "propulsor_active", - "ac|weights*", 'duration'] - - self.add_subsystem('propmodel', - TwinSeriesHybridElectricThermalPropulsionSystem(num_nodes=nn), - promotes_inputs=propulsion_promotes_inputs, - promotes_outputs=propulsion_promotes_outputs) - self.connect('proprpm', ['propmodel.prop1.rpm', 'propmodel.prop2.rpm']) - self.connect('hybrid_factor.vec', 'propmodel.hybrid_split.power_split_fraction') + controls.add_output("hybridization", val=1.0) + + hybrid_factor = self.add_subsystem( + "hybrid_factor", + LinearInterpolator(num_nodes=nn), + promotes_inputs=[("start_val", "hybridization"), ("end_val", "hybridization")], + ) + + propulsion_promotes_outputs = ["fuel_flow", "thrust", "ac|propulsion|thermal|duct|area_nozzle"] + propulsion_promotes_inputs = [ + "fltcond|*", + "ac|propulsion|*", + "throttle", + "propulsor_active", + "ac|weights*", + "duration", + ] + + self.add_subsystem( + "propmodel", + TwinSeriesHybridElectricThermalPropulsionSystem(num_nodes=nn), + promotes_inputs=propulsion_promotes_inputs, + promotes_outputs=propulsion_promotes_outputs, + ) + self.connect("proprpm", ["propmodel.prop1.rpm", "propmodel.prop2.rpm"]) + self.connect("hybrid_factor.vec", "propmodel.hybrid_split.power_split_fraction") # use a different drag coefficient for takeoff versus cruise - if flight_phase not in ['v0v1', 'v1v0', 'v1vr', 'rotate']: - cd0_source = 'ac|aero|polar|CD0_cruise' + if flight_phase not in ["v0v1", "v1v0", "v1vr", "rotate"]: + cd0_source = "ac|aero|polar|CD0_cruise" else: - cd0_source = 'ac|aero|polar|CD0_TO' - self.add_subsystem('drag', PolarDrag(num_nodes=nn), - promotes_inputs=['fltcond|CL', 'ac|geom|*', ('CD0', cd0_source), - 'fltcond|q', ('e', 'ac|aero|polar|e')]) - - self.add_subsystem('OEW',TwinSeriesHybridEmptyWeight(), - promotes_inputs=['*', ('P_TO','ac|propulsion|engine|rating')]) - self.connect('propmodel.propellers_weight', 'W_propeller') - self.connect('propmodel.eng1.component_weight', 'W_engine') - self.connect('propmodel.gen1.component_weight', 'W_generator') - self.connect('propmodel.motors_weight', 'W_motors') + cd0_source = "ac|aero|polar|CD0_TO" + self.add_subsystem( + "drag", + PolarDrag(num_nodes=nn), + promotes_inputs=["fltcond|CL", "ac|geom|*", ("CD0", cd0_source), "fltcond|q", ("e", "ac|aero|polar|e")], + ) + + self.add_subsystem( + "OEW", TwinSeriesHybridEmptyWeight(), promotes_inputs=["*", ("P_TO", "ac|propulsion|engine|rating")] + ) + self.connect("propmodel.propellers_weight", "W_propeller") + self.connect("propmodel.eng1.component_weight", "W_engine") + self.connect("propmodel.gen1.component_weight", "W_generator") + self.connect("propmodel.motors_weight", "W_motors") hxadder = AddSubtractComp() - hxadder.add_equation('OEW',['OEW_orig','W_hx','W_coolant'],scaling_factors=[1,1,1],units='kg') - hxadder.add_equation('drag',['drag_orig','drag_hx'], vec_size=nn, units='N', scaling_factors=[1,1]) - hxadder.add_equation('area_constraint',['hx_frontal_area','nozzle_area'],units='m**2',scaling_factors=[1,-1]) - self.add_subsystem('hxadder',hxadder, promotes_inputs=[('W_coolant','ac|propulsion|thermal|hx|coolant_mass')],promotes_outputs=['OEW','drag']) - self.connect('drag.drag','hxadder.drag_orig') - self.connect('OEW.OEW','hxadder.OEW_orig') - self.connect('propmodel.hx.component_weight','hxadder.W_hx') - self.connect('propmodel.duct.drag','hxadder.drag_hx') - self.connect('propmodel.hx.frontal_area','hxadder.hx_frontal_area') - self.add_subsystem('nozzle_area', MaxComp(num_nodes=nn, units='m**2')) - self.connect('ac|propulsion|thermal|duct|area_nozzle','nozzle_area.array') - self.connect('nozzle_area.max','hxadder.nozzle_area') - intfuel = self.add_subsystem('intfuel', Integrator(num_nodes=nn, method='simpson', diff_units='s', - time_setup='duration'), promotes_inputs=['*'], promotes_outputs=['*']) - intfuel.add_integrand('fuel_used', rate_name='fuel_flow', val=1.0, units='kg') - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|weights|MTOW', 'fuel_used'], - units='kg', vec_size=[1, nn], - scaling_factors=[1, -1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) + hxadder.add_equation("OEW", ["OEW_orig", "W_hx", "W_coolant"], scaling_factors=[1, 1, 1], units="kg") + hxadder.add_equation("drag", ["drag_orig", "drag_hx"], vec_size=nn, units="N", scaling_factors=[1, 1]) + hxadder.add_equation( + "area_constraint", ["hx_frontal_area", "nozzle_area"], units="m**2", scaling_factors=[1, -1] + ) + self.add_subsystem( + "hxadder", + hxadder, + promotes_inputs=[("W_coolant", "ac|propulsion|thermal|hx|coolant_mass")], + promotes_outputs=["OEW", "drag"], + ) + self.connect("drag.drag", "hxadder.drag_orig") + self.connect("OEW.OEW", "hxadder.OEW_orig") + self.connect("propmodel.hx.component_weight", "hxadder.W_hx") + self.connect("propmodel.duct.drag", "hxadder.drag_hx") + self.connect("propmodel.hx.frontal_area", "hxadder.hx_frontal_area") + self.add_subsystem("nozzle_area", MaxComp(num_nodes=nn, units="m**2")) + self.connect("ac|propulsion|thermal|duct|area_nozzle", "nozzle_area.array") + self.connect("nozzle_area.max", "hxadder.nozzle_area") + intfuel = self.add_subsystem( + "intfuel", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + intfuel.add_integrand("fuel_used", rate_name="fuel_flow", val=1.0, units="kg") + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|weights|MTOW", "fuel_used"], + units="kg", + vec_size=[1, nn], + scaling_factors=[1, -1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) + class ElectricTwinAnalysisGroup(Group): - """This is an example of a balanced field takeoff and three-phase mission analysis. - """ + """This is an example of a balanced field takeoff and three-phase mission analysis.""" + def setup(self): # Define number of analysis points to run pers mission segment nn = 11 # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|fuselage|S_wet') - dv_comp.add_output_from_dict('ac|geom|fuselage|width') - dv_comp.add_output_from_dict('ac|geom|fuselage|length') - dv_comp.add_output_from_dict('ac|geom|fuselage|height') - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - dv_comp.add_output_from_dict('ac|weights|W_battery') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - dv_comp.add_output_from_dict('ac|propulsion|propeller|diameter') - dv_comp.add_output_from_dict('ac|propulsion|generator|rating') - dv_comp.add_output_from_dict('ac|propulsion|motor|rating') - - dv_comp.add_output('ac|propulsion|thermal|hx|coolant_mass',val=10.,units='kg') - dv_comp.add_output('ac|propulsion|thermal|hx|channel_width',val=1.,units='mm') - dv_comp.add_output('ac|propulsion|thermal|hx|channel_height',20.,units='mm') - dv_comp.add_output('ac|propulsion|thermal|hx|channel_length',val=0.2,units='m') - dv_comp.add_output('ac|propulsion|thermal|hx|n_parallel',val=50,units=None) + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|fuselage|S_wet") + dv_comp.add_output_from_dict("ac|geom|fuselage|width") + dv_comp.add_output_from_dict("ac|geom|fuselage|length") + dv_comp.add_output_from_dict("ac|geom|fuselage|height") + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + dv_comp.add_output_from_dict("ac|weights|W_battery") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + dv_comp.add_output_from_dict("ac|propulsion|propeller|diameter") + dv_comp.add_output_from_dict("ac|propulsion|generator|rating") + dv_comp.add_output_from_dict("ac|propulsion|motor|rating") + + dv_comp.add_output("ac|propulsion|thermal|hx|coolant_mass", val=10.0, units="kg") + dv_comp.add_output("ac|propulsion|thermal|hx|channel_width", val=1.0, units="mm") + dv_comp.add_output("ac|propulsion|thermal|hx|channel_height", 20.0, units="mm") + dv_comp.add_output("ac|propulsion|thermal|hx|channel_length", val=0.2, units="m") + dv_comp.add_output("ac|propulsion|thermal|hx|n_parallel", val=50, units=None) # dv_comp.add_output('ac|propulsion|thermal|duct|area_nozzle',val=58.*np.ones((nn,)),units='inch**2') - dv_comp.add_output('ac|propulsion|thermal|hx|n_wide_cold',val=430,units=None) - dv_comp.add_output('ac|propulsion|battery|specific_energy',val=300,units='W*h/kg') + dv_comp.add_output("ac|propulsion|thermal|hx|n_wide_cold", val=430, units=None) + dv_comp.add_output("ac|propulsion|battery|specific_energy", val=300, units="W*h/kg") - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') - dv_comp.add_output_from_dict('ac|num_engines') + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") + dv_comp.add_output_from_dict("ac|num_engines") - mission_data_comp = self.add_subsystem('mission_data_comp',IndepVarComp(),promotes_outputs=["*"]) - mission_data_comp.add_output('batt_soc_target', val=0.1, units=None) - mission_data_comp.add_output('T_motor_initial', val=15, units='degC') - mission_data_comp.add_output('T_res_initial', val=15.1, units='degC') - mission_data_comp.add_output('T_batt_initial', val=10.1, units='degC') + mission_data_comp = self.add_subsystem("mission_data_comp", IndepVarComp(), promotes_outputs=["*"]) + mission_data_comp.add_output("batt_soc_target", val=0.1, units=None) + mission_data_comp.add_output("T_motor_initial", val=15, units="degC") + mission_data_comp.add_output("T_res_initial", val=15.1, units="degC") + mission_data_comp.add_output("T_batt_initial", val=10.1, units="degC") # Ensure that any state variables are connected across the mission as intended - analysis = self.add_subsystem('analysis',FullMissionAnalysis(num_nodes=nn, - aircraft_model=SeriesHybridTwinModel), - promotes_inputs=['*'],promotes_outputs= - ['*']) - - margins = self.add_subsystem('margins',ExecComp('MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload', - MTOW_margin={'units':'lbm','val':100}, - MTOW={'units':'lbm','val':10000}, - OEW={'units':'lbm','val':5000}, - total_fuel={'units':'lbm','val':1000}, - W_battery={'units':'lbm','val':1000}, - payload={'units':'lbm','val':1000}), - promotes_inputs=['payload']) - self.connect('cruise.OEW','margins.OEW') - self.connect('descent.fuel_used_final','margins.total_fuel') - self.connect('ac|weights|MTOW','margins.MTOW') - self.connect('ac|weights|W_battery','margins.W_battery') - - augobj = self.add_subsystem('aug_obj', AugmentedFBObjective(), promotes_outputs=['mixed_objective']) - self.connect('ac|weights|MTOW','aug_obj.ac|weights|MTOW') - self.connect('descent.fuel_used_final','aug_obj.fuel_burn') - - self.connect('T_motor_initial','v0v1.propmodel.motorheatsink.T_initial') - self.connect('T_res_initial','v0v1.propmodel.reservoir.T_initial') - self.connect('T_batt_initial','v0v1.propmodel.batteryheatsink.T_initial') + analysis = self.add_subsystem( + "analysis", + FullMissionAnalysis(num_nodes=nn, aircraft_model=SeriesHybridTwinModel), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + + margins = self.add_subsystem( + "margins", + ExecComp( + "MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload", + MTOW_margin={"units": "lbm", "val": 100}, + MTOW={"units": "lbm", "val": 10000}, + OEW={"units": "lbm", "val": 5000}, + total_fuel={"units": "lbm", "val": 1000}, + W_battery={"units": "lbm", "val": 1000}, + payload={"units": "lbm", "val": 1000}, + ), + promotes_inputs=["payload"], + ) + self.connect("cruise.OEW", "margins.OEW") + self.connect("descent.fuel_used_final", "margins.total_fuel") + self.connect("ac|weights|MTOW", "margins.MTOW") + self.connect("ac|weights|W_battery", "margins.W_battery") + + augobj = self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) + self.connect("ac|weights|MTOW", "aug_obj.ac|weights|MTOW") + self.connect("descent.fuel_used_final", "aug_obj.fuel_burn") + + self.connect("T_motor_initial", "v0v1.propmodel.motorheatsink.T_initial") + self.connect("T_res_initial", "v0v1.propmodel.reservoir.T_initial") + self.connect("T_batt_initial", "v0v1.propmodel.batteryheatsink.T_initial") + def configure_problem(): prob = Problem() - prob.model= ElectricTwinAnalysisGroup() - prob.model.nonlinear_solver=NewtonSolver(iprint=2) - prob.model.options['assembled_jac_type'] = 'csc' + prob.model = ElectricTwinAnalysisGroup() + prob.model.nonlinear_solver = NewtonSolver(iprint=2) + prob.model.options["assembled_jac_type"] = "csc" prob.model.linear_solver = DirectSolver(assemble_jac=True) - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.nonlinear_solver.options['maxiter'] = 10 - prob.model.nonlinear_solver.options['atol'] = 1e-8 - prob.model.nonlinear_solver.options['rtol'] = 1e-8 + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.nonlinear_solver.options["maxiter"] = 10 + prob.model.nonlinear_solver.options["atol"] = 1e-8 + prob.model.nonlinear_solver.options["rtol"] = 1e-8 return prob + def set_values(prob, num_nodes, design_range, spec_energy): # set some (required) mission parameters. Each pahse needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('climb.fltcond|vs', np.ones((num_nodes,))*1500, units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.ones((num_nodes,))*124, units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,))*0.01, units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.ones((num_nodes,))*170, units='kn') - prob.set_val('descent.fltcond|vs', np.ones((num_nodes,))*(-600), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,))*140, units='kn') - - prob.set_val('cruise|h0',29000,units='ft') - prob.set_val('mission_range',design_range,units='NM') - prob.set_val('payload',1000,units='lb') - prob.set_val('ac|propulsion|battery|specific_energy', spec_energy, units='W*h/kg') + prob.set_val("climb.fltcond|vs", np.ones((num_nodes,)) * 1500, units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.ones((num_nodes,)) * 124, units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 0.01, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.ones((num_nodes,)) * 170, units="kn") + prob.set_val("descent.fltcond|vs", np.ones((num_nodes,)) * (-600), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 140, units="kn") + + prob.set_val("cruise|h0", 29000, units="ft") + prob.set_val("mission_range", design_range, units="NM") + prob.set_val("payload", 1000, units="lb") + prob.set_val("ac|propulsion|battery|specific_energy", spec_energy, units="W*h/kg") # (optional) guesses for takeoff speeds may help with convergence - prob.set_val('v0v1.fltcond|Utrue',np.ones((num_nodes))*50,units='kn') - prob.set_val('v1vr.fltcond|Utrue',np.ones((num_nodes))*85,units='kn') - prob.set_val('v1v0.fltcond|Utrue',np.ones((num_nodes))*85,units='kn') + prob.set_val("v0v1.fltcond|Utrue", np.ones((num_nodes)) * 50, units="kn") + prob.set_val("v1vr.fltcond|Utrue", np.ones((num_nodes)) * 85, units="kn") + prob.set_val("v1v0.fltcond|Utrue", np.ones((num_nodes)) * 85, units="kn") # set some airplane-specific values - prob['analysis.cruise.acmodel.OEW.const.structural_fudge'] = 2.0 - prob['ac|propulsion|propeller|diameter'] = 2.2 - prob['ac|propulsion|engine|rating'] = 1117.2 + prob["analysis.cruise.acmodel.OEW.const.structural_fudge"] = 2.0 + prob["ac|propulsion|propeller|diameter"] = 2.2 + prob["ac|propulsion|engine|rating"] = 1117.2 + def run_hybrid_twin_thermal_analysis(plots=False): prob = configure_problem() prob.setup(check=False) - prob['cruise.hybridization'] = 0.05778372636876463 + prob["cruise.hybridization"] = 0.05778372636876463 set_values(prob, 11, 500, 450) prob.run_model() if plots: show_outputs(prob) return prob - + + def show_outputs(prob): # print some outputs - vars_list = ['ac|weights|MTOW','climb.OEW','descent.fuel_used_final', - 'rotate.range_final','descent.propmodel.batt1.SOC_final','cruise.hybridization', - 'ac|weights|W_battery','margins.MTOW_margin', - 'ac|propulsion|motor|rating','ac|propulsion|generator|rating','ac|propulsion|engine|rating', - 'ac|geom|wing|S_ref','v0v1.Vstall_eas','v0v1.takeoff|vr', - 'engineoutclimb.gamma', - 'cruise.propmodel.duct.drag', 'ac|propulsion|thermal|hx|coolant_mass', - 'climb.propmodel.duct.mdot'] - units = ['lb','lb','lb', - 'ft', None, None, - 'lb','lb', - 'hp','hp','hp', - 'ft**2','kn','kn', - 'deg', - 'lbf','lb', - 'lb/s'] - nice_print_names = ['MTOW', 'OEW', 'Fuel used', - 'TOFL (over 35ft obstacle)', 'Final state of charge', 'Cruise hybridization', - 'Battery weight','MTOW margin', - 'Motor rating', 'Generator rating', 'Engine rating', - 'Wing area', 'Stall speed', 'Rotate speed', - 'Engine out climb angle', - 'Coolant duct cruise drag', 'Coolant mass', - 'Coolant duct mass flow'] + vars_list = [ + "ac|weights|MTOW", + "climb.OEW", + "descent.fuel_used_final", + "rotate.range_final", + "descent.propmodel.batt1.SOC_final", + "cruise.hybridization", + "ac|weights|W_battery", + "margins.MTOW_margin", + "ac|propulsion|motor|rating", + "ac|propulsion|generator|rating", + "ac|propulsion|engine|rating", + "ac|geom|wing|S_ref", + "v0v1.Vstall_eas", + "v0v1.takeoff|vr", + "engineoutclimb.gamma", + "cruise.propmodel.duct.drag", + "ac|propulsion|thermal|hx|coolant_mass", + "climb.propmodel.duct.mdot", + ] + units = [ + "lb", + "lb", + "lb", + "ft", + None, + None, + "lb", + "lb", + "hp", + "hp", + "hp", + "ft**2", + "kn", + "kn", + "deg", + "lbf", + "lb", + "lb/s", + ] + nice_print_names = [ + "MTOW", + "OEW", + "Fuel used", + "TOFL (over 35ft obstacle)", + "Final state of charge", + "Cruise hybridization", + "Battery weight", + "MTOW margin", + "Motor rating", + "Generator rating", + "Engine rating", + "Wing area", + "Stall speed", + "Rotate speed", + "Engine out climb angle", + "Coolant duct cruise drag", + "Coolant mass", + "Coolant duct mass flow", + ] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+str(units[i])) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + str(units[i])) - print('Motor temps (climb): '+str(prob.get_val('climb.propmodel.motorheatsink.T',units='degC'))) - print('Battery temps (climb): '+str(prob.get_val('climb.propmodel.batteryheatsink.T',units='degC'))) + print("Motor temps (climb): " + str(prob.get_val("climb.propmodel.motorheatsink.T", units="degC"))) + print("Battery temps (climb): " + str(prob.get_val("climb.propmodel.batteryheatsink.T", units="degC"))) # plot some stuff plots = True if plots: - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs','propmodel.batt1.SOC', - 'propmodel.motorheatsink.T','propmodel.batteryheatsink.T','propmodel.reservoir.T_out'] - y_units = ['ft','kn','lbm',None,'ft/min',None,'degC','degC','degC'] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', 'Vertical speed (ft/min)', 'Battery SOC', 'Motor temp', 'Battery temp','Reservoir outlet temp'] - phases = ['v0v1','v1vr','v1v0','rotate'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='o', - plot_title='Takeoff Profile') - - phases = ['v0v1','v1vr','rotate','climb','cruise','descent'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='Full Mission Profile') + x_var = "range" + x_unit = "NM" + y_vars = [ + "fltcond|h", + "fltcond|Ueas", + "fuel_used", + "throttle", + "fltcond|vs", + "propmodel.batt1.SOC", + "propmodel.motorheatsink.T", + "propmodel.batteryheatsink.T", + "propmodel.reservoir.T_out", + ] + y_units = ["ft", "kn", "lbm", None, "ft/min", None, "degC", "degC", "degC"] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + "Battery SOC", + "Motor temp", + "Battery temp", + "Reservoir outlet temp", + ] + phases = ["v0v1", "v1vr", "v1v0", "rotate"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="o", + plot_title="Takeoff Profile", + ) + + phases = ["v0v1", "v1vr", "rotate", "climb", "cruise", "descent"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="Full Mission Profile", + ) + if __name__ == "__main__": # for run type choose choose optimization, comp_sizing, or analysis - run_type = 'example' + run_type = "example" num_nodes = 11 - if run_type == 'example': + if run_type == "example": # runs a default analysis-only mission (no optimization) run_hybrid_twin_thermal_analysis(plots=True) else: # can run a sweep of design range and spec energy (not tested) - #design_ranges = [300,350,400,450,500,550,600,650,700] - #specific_energies = [250,300,350,400,450,500,550,600,650,700,750,800] + # design_ranges = [300,350,400,450,500,550,600,650,700] + # specific_energies = [250,300,350,400,450,500,550,600,650,700,750,800] # or a single point design_ranges = [500] @@ -307,7 +443,7 @@ def show_outputs(prob): write_logs = False if write_logs: - logging.basicConfig(filename='opt.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s') + logging.basicConfig(filename="opt.log", filemode="w", format="%(name)s - %(levelname)s - %(message)s") last_successful_opt = None # run a sweep of cases at various specific energies and ranges @@ -316,118 +452,152 @@ def show_outputs(prob): try: prob = configure_problem() spec_energy = this_spec_energy - if run_type == 'optimization': - print('======Performing Multidisciplinary Design Optimization===========') - prob.model.add_design_var('ac|weights|MTOW', lower=4000, upper=5700) - prob.model.add_design_var('ac|geom|wing|S_ref',lower=15,upper=40) - prob.model.add_design_var('ac|propulsion|engine|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|motor|rating',lower=450,upper=3000) - prob.model.add_design_var('ac|propulsion|generator|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|weights|W_battery',lower=20,upper=2250) - prob.model.add_design_var('ac|weights|W_fuel_max',lower=500,upper=3000) - prob.model.add_design_var('cruise.hybridization', lower=0.001, upper=0.999) - prob.model.add_design_var('climb.hybridization', lower=0.001, upper=0.999) - prob.model.add_design_var('descent.hybridization', lower=0.01, upper=1.0) - prob.model.add_design_var('ac|propulsion|thermal|duct|area_nozzle', lower=1., upper=200.) - prob.model.add_design_var('ac|propulsion|thermal|hx|n_wide_cold', lower=100., upper=1500.) - prob.model.add_constraint('margins.MTOW_margin',lower=0.0) - prob.model.add_design_var('ac|propulsion|thermal|hx|coolant_mass',lower=5.0,upper=15.0) + if run_type == "optimization": + print("======Performing Multidisciplinary Design Optimization===========") + prob.model.add_design_var("ac|weights|MTOW", lower=4000, upper=5700) + prob.model.add_design_var("ac|geom|wing|S_ref", lower=15, upper=40) + prob.model.add_design_var("ac|propulsion|engine|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|motor|rating", lower=450, upper=3000) + prob.model.add_design_var("ac|propulsion|generator|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|weights|W_battery", lower=20, upper=2250) + prob.model.add_design_var("ac|weights|W_fuel_max", lower=500, upper=3000) + prob.model.add_design_var("cruise.hybridization", lower=0.001, upper=0.999) + prob.model.add_design_var("climb.hybridization", lower=0.001, upper=0.999) + prob.model.add_design_var("descent.hybridization", lower=0.01, upper=1.0) + prob.model.add_design_var("ac|propulsion|thermal|duct|area_nozzle", lower=1.0, upper=200.0) + prob.model.add_design_var("ac|propulsion|thermal|hx|n_wide_cold", lower=100.0, upper=1500.0) + prob.model.add_constraint("margins.MTOW_margin", lower=0.0) + prob.model.add_design_var("ac|propulsion|thermal|hx|coolant_mass", lower=5.0, upper=15.0) # prob.model.add_constraint('design_mission.residuals.fuel_capacity_margin',lower=0.0) - prob.model.add_constraint('rotate.range_final',upper=1357) - prob.model.add_constraint('v0v1.Vstall_eas',upper=42.0) - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_constraint('climb.throttle',upper=1.05*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('descent.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('engineoutclimb.gamma',lower=0.02) - prob.model.add_constraint('climb.propmodel.motorheatsink.T',upper=363.*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.motorheatsink.T',upper=363.*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.batteryheatsink.T',upper=323.*np.ones(num_nodes)) - prob.model.add_constraint('cruise.propmodel.batteryheatsink.T',upper=323.*np.ones(num_nodes)) - prob.model.add_constraint('climb.hxadder.area_constraint',lower=0.001) - prob.model.add_objective('mixed_objective') # TODO add this objective - - elif run_type == 'comp_sizing': - print('======Performing Component Sizing Optimization===========') - prob.model.add_design_var('ac|propulsion|engine|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|motor|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|propulsion|generator|rating',lower=1,upper=3000) - prob.model.add_design_var('ac|weights|W_battery',lower=20,upper=2250) - prob.model.add_design_var('cruise.hybridization', lower=0.01, upper=0.5) - - prob.model.add_constraint('margins.MTOW_margin',equals=0.0) # TODO implement - prob.model.add_constraint('rotate.range_final',upper=1357) # TODO check units - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_constraint('v0v1.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('v0v1.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.eng1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.gen1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.propmodel.batt1.component_sizing_margin',upper=1.0*np.ones(num_nodes)) - prob.model.add_constraint('climb.throttle',upper=1.05*np.ones(num_nodes)) - prob.model.add_objective('fuel_burn') + prob.model.add_constraint("rotate.range_final", upper=1357) + prob.model.add_constraint("v0v1.Vstall_eas", upper=42.0) + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_constraint("climb.throttle", upper=1.05 * np.ones(num_nodes)) + prob.model.add_constraint( + "climb.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "cruise.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "descent.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint("engineoutclimb.gamma", lower=0.02) + prob.model.add_constraint("climb.propmodel.motorheatsink.T", upper=363.0 * np.ones(num_nodes)) + prob.model.add_constraint("cruise.propmodel.motorheatsink.T", upper=363.0 * np.ones(num_nodes)) + prob.model.add_constraint("climb.propmodel.batteryheatsink.T", upper=323.0 * np.ones(num_nodes)) + prob.model.add_constraint( + "cruise.propmodel.batteryheatsink.T", upper=323.0 * np.ones(num_nodes) + ) + prob.model.add_constraint("climb.hxadder.area_constraint", lower=0.001) + prob.model.add_objective("mixed_objective") # TODO add this objective + + elif run_type == "comp_sizing": + print("======Performing Component Sizing Optimization===========") + prob.model.add_design_var("ac|propulsion|engine|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|motor|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|propulsion|generator|rating", lower=1, upper=3000) + prob.model.add_design_var("ac|weights|W_battery", lower=20, upper=2250) + prob.model.add_design_var("cruise.hybridization", lower=0.01, upper=0.5) + + prob.model.add_constraint("margins.MTOW_margin", equals=0.0) # TODO implement + prob.model.add_constraint("rotate.range_final", upper=1357) # TODO check units + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_constraint( + "v0v1.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "v0v1.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.eng1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.gen1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint( + "climb.propmodel.batt1.component_sizing_margin", upper=1.0 * np.ones(num_nodes) + ) + prob.model.add_constraint("climb.throttle", upper=1.05 * np.ones(num_nodes)) + prob.model.add_objective("fuel_burn") else: - print('======Analyzing Fuel Burn for Given Mision============') - prob.model.add_design_var('cruise.hybridization', lower=0.01, upper=0.5) - prob.model.add_constraint('descent.propmodel.batt1.SOC_final',lower=0.0) - prob.model.add_objective('descent.fuel_used_final') + print("======Analyzing Fuel Burn for Given Mision============") + prob.model.add_design_var("cruise.hybridization", lower=0.01, upper=0.5) + prob.model.add_constraint("descent.propmodel.batt1.SOC_final", lower=0.0) + prob.model.add_objective("descent.fuel_used_final") prob.driver = ScipyOptimizeDriver() if write_logs: - filename_to_save = 'case_'+str(spec_energy)+'_'+str(design_range)+'.sql' + filename_to_save = "case_" + str(spec_energy) + "_" + str(design_range) + ".sql" if os.path.isfile(filename_to_save): if design_range != 300: last_successful_opt = filename_to_save else: - last_successful_opt = 'case_'+str(spec_energy+50)+'_'+str(700)+'.sql' - print('Skipping '+filename_to_save) + last_successful_opt = "case_" + str(spec_energy + 50) + "_" + str(700) + ".sql" + print("Skipping " + filename_to_save) continue recorder = SqliteRecorder(filename_to_save) prob.driver.add_recorder(recorder) - prob.driver.recording_options['includes'] = [] - prob.driver.recording_options['record_objectives'] = True - prob.driver.recording_options['record_constraints'] = True - prob.driver.recording_options['record_desvars'] = True + prob.driver.recording_options["includes"] = [] + prob.driver.recording_options["record_objectives"] = True + prob.driver.recording_options["record_constraints"] = True + prob.driver.recording_options["record_desvars"] = True prob.setup(check=False) set_values(prob, num_nodes, design_range, spec_energy) if last_successful_opt is not None: cr = CaseReader(last_successful_opt) - driver_cases = cr.list_cases('driver') + driver_cases = cr.list_cases("driver") case = cr.get_case(driver_cases[-1]) design_vars = case.get_design_vars() for key in design_vars.keys(): - prob.set_val(key,design_vars[key]) + prob.set_val(key, design_vars[key]) run_flag = prob.run_driver() if run_flag: - raise ValueError('Opt failed') + raise ValueError("Opt failed") else: if write_logs: last_successful_opt = filename_to_save prob.cleanup() except BaseException as e: if write_logs: - logging.error('Optimization '+filename_to_save+' failed because '+repr(e)) + logging.error("Optimization " + filename_to_save + " failed because " + repr(e)) prob.cleanup() try: if write_logs: - os.rename(filename_to_save, filename_to_save.split('.sql')[0]+'_failed.sql') + os.rename(filename_to_save, filename_to_save.split(".sql")[0] + "_failed.sql") except WindowsError as we: if write_logs: - logging.error('Error renaming file: '+repr(we)) + logging.error("Error renaming file: " + repr(we)) os.remove(filename_to_save) - show_outputs(prob) \ No newline at end of file + show_outputs(prob) diff --git a/openconcept/examples/KingAirC90GT.py b/openconcept/examples/KingAirC90GT.py index 62ef78b3..64751592 100644 --- a/openconcept/examples/KingAirC90GT.py +++ b/openconcept/examples/KingAirC90GT.py @@ -12,189 +12,236 @@ from openconcept.examples.aircraft_data.KingAirC90GT import data as acdata from openconcept.utilities import AddSubtractComp, Integrator, DictIndepVarComp, plot_trajectory + class KingAirC90GTModel(Group): """ A custom model specific to the King Air C90GT airplane This class will be passed in to the mission analysis code. """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] # any control variables other than throttle and braking need to be defined here - controls = self.add_subsystem('controls',IndepVarComp(),promotes_outputs=['*']) - controls.add_output('prop|rpm',val=np.ones((nn,))*1900,units='rpm') + controls = self.add_subsystem("controls", IndepVarComp(), promotes_outputs=["*"]) + controls.add_output("prop|rpm", val=np.ones((nn,)) * 1900, units="rpm") # a propulsion system needs to be defined in order to provide thrust # information for the mission analysis code - propulsion_promotes_outputs = ['fuel_flow','thrust'] + propulsion_promotes_outputs = ["fuel_flow", "thrust"] propulsion_promotes_inputs = ["fltcond|*", "ac|propulsion|*", "throttle", "propulsor_active"] - self.add_subsystem('propmodel',TwinTurbopropPropulsionSystem(num_nodes=nn), - promotes_inputs=propulsion_promotes_inputs, - promotes_outputs=propulsion_promotes_outputs) - self.connect('prop|rpm', ['propmodel.prop1.rpm', 'propmodel.prop2.rpm']) + self.add_subsystem( + "propmodel", + TwinTurbopropPropulsionSystem(num_nodes=nn), + promotes_inputs=propulsion_promotes_inputs, + promotes_outputs=propulsion_promotes_outputs, + ) + self.connect("prop|rpm", ["propmodel.prop1.rpm", "propmodel.prop2.rpm"]) # use a different drag coefficient for takeoff versus cruise - if flight_phase not in ['v0v1', 'v1v0', 'v1vr', 'rotate']: - cd0_source = 'ac|aero|polar|CD0_cruise' + if flight_phase not in ["v0v1", "v1v0", "v1vr", "rotate"]: + cd0_source = "ac|aero|polar|CD0_cruise" else: - cd0_source = 'ac|aero|polar|CD0_TO' - self.add_subsystem('drag', PolarDrag(num_nodes=nn), - promotes_inputs=['fltcond|CL', 'ac|geom|*', ('CD0', cd0_source), - 'fltcond|q', ('e', 'ac|aero|polar|e')], - promotes_outputs=['drag']) + cd0_source = "ac|aero|polar|CD0_TO" + self.add_subsystem( + "drag", + PolarDrag(num_nodes=nn), + promotes_inputs=["fltcond|CL", "ac|geom|*", ("CD0", cd0_source), "fltcond|q", ("e", "ac|aero|polar|e")], + promotes_outputs=["drag"], + ) # generally the weights module will be custom to each airplane - self.add_subsystem('OEW', SingleTurboPropEmptyWeight(), - promotes_inputs=['*', ('P_TO', 'ac|propulsion|engine|rating')], - promotes_outputs=['OEW']) - self.connect('propmodel.propellers_weight', 'W_propeller') - self.connect('propmodel.engines_weight', 'W_engine') + self.add_subsystem( + "OEW", + SingleTurboPropEmptyWeight(), + promotes_inputs=["*", ("P_TO", "ac|propulsion|engine|rating")], + promotes_outputs=["OEW"], + ) + self.connect("propmodel.propellers_weight", "W_propeller") + self.connect("propmodel.engines_weight", "W_engine") # airplanes which consume fuel will need to integrate - # fuel usage across the mission and subtract it from TOW - intfuel = self.add_subsystem('intfuel', Integrator(num_nodes=nn, method='simpson', diff_units='s', - time_setup='duration'), promotes_inputs=['*'], promotes_outputs=['*']) - intfuel.add_integrand('fuel_used', rate_name='fuel_flow', val=1.0, units='kg') - - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|weights|MTOW', 'fuel_used'], - units='kg', vec_size=[1, nn], - scaling_factors=[1, -1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) + # fuel usage across the mission and subtract it from TOW + intfuel = self.add_subsystem( + "intfuel", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + intfuel.add_integrand("fuel_used", rate_name="fuel_flow", val=1.0, units="kg") + + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|weights|MTOW", "fuel_used"], + units="kg", + vec_size=[1, nn], + scaling_factors=[1, -1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) class KingAirAnalysisGroup(Group): - """This is an example of a balanced field takeoff and three-phase mission analysis. - """ + """This is an example of a balanced field takeoff and three-phase mission analysis.""" + def setup(self): # Define number of analysis points to run pers mission segment nn = 11 # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|fuselage|S_wet') - dv_comp.add_output_from_dict('ac|geom|fuselage|width') - dv_comp.add_output_from_dict('ac|geom|fuselage|length') - dv_comp.add_output_from_dict('ac|geom|fuselage|height') - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - dv_comp.add_output_from_dict('ac|propulsion|propeller|diameter') - - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') - dv_comp.add_output_from_dict('ac|num_engines') + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|fuselage|S_wet") + dv_comp.add_output_from_dict("ac|geom|fuselage|width") + dv_comp.add_output_from_dict("ac|geom|fuselage|length") + dv_comp.add_output_from_dict("ac|geom|fuselage|height") + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + dv_comp.add_output_from_dict("ac|propulsion|propeller|diameter") + + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") + dv_comp.add_output_from_dict("ac|num_engines") # Run a full mission analysis including takeoff, climb, cruise, and descent - analysis = self.add_subsystem('analysis', - FullMissionAnalysis(num_nodes=nn, - aircraft_model=KingAirC90GTModel), - promotes_inputs=['*'], promotes_outputs=['*']) + analysis = self.add_subsystem( + "analysis", + FullMissionAnalysis(num_nodes=nn, aircraft_model=KingAirC90GTModel), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) def configure_problem(): prob = Problem() prob.model = KingAirAnalysisGroup() prob.model.nonlinear_solver = NewtonSolver(iprint=2) - prob.model.options['assembled_jac_type'] = 'csc' + prob.model.options["assembled_jac_type"] = "csc" prob.model.linear_solver = DirectSolver(assemble_jac=True) - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.nonlinear_solver.options['maxiter'] = 10 - prob.model.nonlinear_solver.options['atol'] = 1e-6 - prob.model.nonlinear_solver.options['rtol'] = 1e-6 + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.nonlinear_solver.options["maxiter"] = 10 + prob.model.nonlinear_solver.options["atol"] = 1e-6 + prob.model.nonlinear_solver.options["rtol"] = 1e-6 # prob.model.nonlinear_solver.linesearch = BoundsEnforceLS(bound_enforcement='scalar', print_bound_enforce=False) return prob + def show_outputs(prob): # print some outputs - vars_list = ['ac|weights|MTOW','climb.OEW','descent.fuel_used_final','rotate.range_final'] - units = ['lb','lb','lb','ft'] - nice_print_names = ['MTOW', 'OEW', 'Fuel used', 'TOFL (over 35ft obstacle)'] + vars_list = ["ac|weights|MTOW", "climb.OEW", "descent.fuel_used_final", "rotate.range_final"] + units = ["lb", "lb", "lb", "ft"] + nice_print_names = ["MTOW", "OEW", "Fuel used", "TOFL (over 35ft obstacle)"] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+units[i]) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + units[i]) # plot some stuff plots = True if plots: - x_var = 'range' - x_unit = 'ft' - y_vars = ['fltcond|Ueas', 'fltcond|h'] - y_units = ['kn', 'ft'] - x_label = 'Distance (ft)' - y_labels = ['Veas airspeed (knots)', 'Altitude (ft)'] - phases = ['v0v1', 'v1vr', 'rotate', 'v1v0'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, - plot_title='King Air Takeoff') - - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs'] - y_units = ['ft','kn','lbm',None,'ft/min'] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', 'Vertical speed (ft/min)'] - phases = ['climb', 'cruise', 'descent'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='King Air Mission Profile') + x_var = "range" + x_unit = "ft" + y_vars = ["fltcond|Ueas", "fltcond|h"] + y_units = ["kn", "ft"] + x_label = "Distance (ft)" + y_labels = ["Veas airspeed (knots)", "Altitude (ft)"] + phases = ["v0v1", "v1vr", "rotate", "v1v0"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + plot_title="King Air Takeoff", + ) + + x_var = "range" + x_unit = "NM" + y_vars = ["fltcond|h", "fltcond|Ueas", "fuel_used", "throttle", "fltcond|vs"] + y_units = ["ft", "kn", "lbm", None, "ft/min"] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + ] + phases = ["climb", "cruise", "descent"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="King Air Mission Profile", + ) + def set_values(prob, num_nodes): # set some (required) mission parameters. Each pahse needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('climb.fltcond|vs', np.ones((num_nodes,))*1500, units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.ones((num_nodes,))*124, units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,))*0.01, units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.ones((num_nodes,))*170, units='kn') - prob.set_val('descent.fltcond|vs', np.ones((num_nodes,))*(-600), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,))*140, units='kn') + prob.set_val("climb.fltcond|vs", np.ones((num_nodes,)) * 1500, units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.ones((num_nodes,)) * 124, units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 0.01, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.ones((num_nodes,)) * 170, units="kn") + prob.set_val("descent.fltcond|vs", np.ones((num_nodes,)) * (-600), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 140, units="kn") - prob.set_val('cruise|h0',29000,units='ft') - prob.set_val('mission_range',1000,units='NM') - prob.set_val('payload',1000,units='lb') + prob.set_val("cruise|h0", 29000, units="ft") + prob.set_val("mission_range", 1000, units="NM") + prob.set_val("payload", 1000, units="lb") # (optional) guesses for takeoff speeds may help with convergence - prob.set_val('v0v1.fltcond|Utrue',np.ones((num_nodes))*50,units='kn') - prob.set_val('v1vr.fltcond|Utrue',np.ones((num_nodes))*85,units='kn') - prob.set_val('v1v0.fltcond|Utrue',np.ones((num_nodes))*85,units='kn') + prob.set_val("v0v1.fltcond|Utrue", np.ones((num_nodes)) * 50, units="kn") + prob.set_val("v1vr.fltcond|Utrue", np.ones((num_nodes)) * 85, units="kn") + prob.set_val("v1v0.fltcond|Utrue", np.ones((num_nodes)) * 85, units="kn") # set some airplane-specific values. The throttle edits are to derate the takeoff power of the PT6A - prob['climb.OEW.structural_fudge'] = 1.67 - prob['v0v1.throttle'] = np.ones((num_nodes)) * 0.75 - prob['v1vr.throttle'] = np.ones((num_nodes)) * 0.75 - prob['rotate.throttle'] = np.ones((num_nodes)) * 0.75 + prob["climb.OEW.structural_fudge"] = 1.67 + prob["v0v1.throttle"] = np.ones((num_nodes)) * 0.75 + prob["v1vr.throttle"] = np.ones((num_nodes)) * 0.75 + prob["rotate.throttle"] = np.ones((num_nodes)) * 0.75 + def run_kingair_analysis(plots=False): num_nodes = 11 prob = configure_problem() - prob.setup(check=True, mode='fwd') + prob.setup(check=True, mode="fwd") set_values(prob, num_nodes) prob.run_model() if plots: @@ -204,4 +251,3 @@ def run_kingair_analysis(plots=False): if __name__ == "__main__": run_kingair_analysis(plots=True) - diff --git a/openconcept/examples/N3_HybridSingleAisle_Refrig.py b/openconcept/examples/N3_HybridSingleAisle_Refrig.py index 4c17acd7..678002bf 100644 --- a/openconcept/examples/N3_HybridSingleAisle_Refrig.py +++ b/openconcept/examples/N3_HybridSingleAisle_Refrig.py @@ -20,398 +20,592 @@ LiquidCooledMotor, ) + class HybridSingleAisleModel(IntegratorGroup): """ Model for NASA N+3 twin hybrid single aisle study """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('flight_phase', default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) def setup(self): - nn = self.options['num_nodes'] - flight_phase = self.options['flight_phase'] + nn = self.options["num_nodes"] + flight_phase = self.options["flight_phase"] - #=============AERODYNAMICS====================== + # =============AERODYNAMICS====================== # use a different drag coefficient for takeoff versus cruise - if flight_phase not in ['v0v1', 'v1v0', 'v1vr', 'rotate']: - cd0_source = 'ac|aero|polar|CD0_cruise' + if flight_phase not in ["v0v1", "v1v0", "v1vr", "rotate"]: + cd0_source = "ac|aero|polar|CD0_cruise" else: - cd0_source = 'ac|aero|polar|CD0_TO' - - self.add_subsystem('airframe_drag', PolarDrag(num_nodes=nn), - promotes_inputs=['fltcond|CL', 'ac|geom|*', ('CD0', cd0_source), - 'fltcond|q', ('e', 'ac|aero|polar|e')]) - self.promote_add(sources=['airframe_drag.drag', 'variable_duct.force.F_net', 'motor_duct.force.F_net'], - prom_name='drag', factors=[1.0, -2.0, -2.0], vec_size=nn, units='N') - - - #=============PROPULSION======================= + cd0_source = "ac|aero|polar|CD0_TO" + + self.add_subsystem( + "airframe_drag", + PolarDrag(num_nodes=nn), + promotes_inputs=["fltcond|CL", "ac|geom|*", ("CD0", cd0_source), "fltcond|q", ("e", "ac|aero|polar|e")], + ) + self.promote_add( + sources=["airframe_drag.drag", "variable_duct.force.F_net", "motor_duct.force.F_net"], + prom_name="drag", + factors=[1.0, -2.0, -2.0], + vec_size=nn, + units="N", + ) + + # =============PROPULSION======================= # Hybrid propulsion motor (model one side only) - self.add_subsystem('hybrid_throttle',LinearInterpolator(num_nodes=nn, units=None), promotes_inputs=[('start_val','hybrid_throttle_start'), - ('end_val','hybrid_throttle_end')]) - - self.add_subsystem('hybrid_motor', SimpleMotor(num_nodes=nn, efficiency=0.95), - promotes_inputs=[('elec_power_rating','ac|propulsion|motor|rating')]) - self.connect('hybrid_motor.shaft_power_out', 'engine.hybrid_power') - self.connect('hybrid_throttle.vec','hybrid_motor.throttle') + self.add_subsystem( + "hybrid_throttle", + LinearInterpolator(num_nodes=nn, units=None), + promotes_inputs=[("start_val", "hybrid_throttle_start"), ("end_val", "hybrid_throttle_end")], + ) + + self.add_subsystem( + "hybrid_motor", + SimpleMotor(num_nodes=nn, efficiency=0.95), + promotes_inputs=[("elec_power_rating", "ac|propulsion|motor|rating")], + ) + self.connect("hybrid_motor.shaft_power_out", "engine.hybrid_power") + self.connect("hybrid_throttle.vec", "hybrid_motor.throttle") # Add a surrogate model for the engine. Inputs are Mach, Alt, Throttle, Hybrid power - self.add_subsystem('engine', N3Hybrid(num_nodes=nn, plot=False), - promotes_inputs=["fltcond|*", "throttle"]) + self.add_subsystem("engine", N3Hybrid(num_nodes=nn, plot=False), promotes_inputs=["fltcond|*", "throttle"]) # double the thrust and fuel flow of the engine and integrate fuel flow - self.promote_mult('engine.thrust', prom_name='thrust', factor=2.0, vec_size=nn, units='kN') - self.promote_mult('engine.fuel_flow', prom_name='fuel_flow', factor=2.0, vec_size=nn, units='kg/s', - tags=['integrate', 'state_name:fuel_used', 'state_units:kg', 'state_val:1.0', 'state_promotes:True']) + self.promote_mult("engine.thrust", prom_name="thrust", factor=2.0, vec_size=nn, units="kN") + self.promote_mult( + "engine.fuel_flow", + prom_name="fuel_flow", + factor=2.0, + vec_size=nn, + units="kg/s", + tags=["integrate", "state_name:fuel_used", "state_units:kg", "state_val:1.0", "state_promotes:True"], + ) # Hybrid propulsion battery - self.add_subsystem('battery', SOCBattery(num_nodes=nn, efficiency=0.95, specific_energy=400), - promotes_inputs=[('battery_weight','ac|propulsion|battery|weight')]) - self.promote_add(sources=['hybrid_motor.elec_load', 'refrig.elec_load', 'motorfaultprot.elec_load','battery_coolant_pump.elec_load', 'motor_coolant_pump.elec_load'], - prom_name='elec_load', factors=[1.0, 1.0, 1.0, 1.0, 1.0], vec_size=nn, val=1.0, units='kW') - self.connect('elec_load', 'battery.elec_load') - - #=============THERMAL====================== - thermal_params = self.add_subsystem('thermal_params',om.IndepVarComp(), promotes_outputs=['*']) + self.add_subsystem( + "battery", + SOCBattery(num_nodes=nn, efficiency=0.95, specific_energy=400), + promotes_inputs=[("battery_weight", "ac|propulsion|battery|weight")], + ) + self.promote_add( + sources=[ + "hybrid_motor.elec_load", + "refrig.elec_load", + "motorfaultprot.elec_load", + "battery_coolant_pump.elec_load", + "motor_coolant_pump.elec_load", + ], + prom_name="elec_load", + factors=[1.0, 1.0, 1.0, 1.0, 1.0], + vec_size=nn, + val=1.0, + units="kW", + ) + self.connect("elec_load", "battery.elec_load") + + # =============THERMAL====================== + thermal_params = self.add_subsystem("thermal_params", om.IndepVarComp(), promotes_outputs=["*"]) # properties - thermal_params.add_output('rho_coolant', val=1020*np.ones((nn,)),units='kg/m**3') + thermal_params.add_output("rho_coolant", val=1020 * np.ones((nn,)), units="kg/m**3") # controls - thermal_params.add_output('mdot_coolant_battery', val=4.8*np.ones((nn,)), units='kg/s') - thermal_params.add_output('mdot_coolant_motor', val=1.2*np.ones((nn,)), units='kg/s') + thermal_params.add_output("mdot_coolant_battery", val=4.8 * np.ones((nn,)), units="kg/s") + thermal_params.add_output("mdot_coolant_motor", val=1.2 * np.ones((nn,)), units="kg/s") # fault protection needs separate cooler because it needs 40C inflow temp at 3gpm - thermal_params.add_output('mdot_coolant_fault_prot', val=0.19*np.ones((nn,)), units='kg/s') - - thermal_params.add_output('bypass_heat_pump', val=np.ones((nn,))) - thermal_params.add_output('variable_duct_nozzle_area_start', val=20, units='inch**2') - thermal_params.add_output('variable_duct_nozzle_area_end', val=20, units='inch**2') - thermal_params.add_output('heat_pump_specific_power', val=200., units='W/kg') - thermal_params.add_output('heat_pump_eff_factor', val=0.4, units=None) - - self.add_subsystem('li_battery',LinearInterpolator(num_nodes=nn, units='inch**2'), promotes_outputs=[('vec', 'variable_duct_nozzle_area')]) - self.connect('variable_duct_nozzle_area_start','li_battery.start_val') - self.connect('variable_duct_nozzle_area_end','li_battery.end_val') - self.add_subsystem('li_motor',LinearInterpolator(num_nodes=nn, units='inch**2'), promotes_inputs=[('start_val','ac|propulsion|thermal|hx_motor|nozzle_area'), - ('end_val','ac|propulsion|thermal|hx_motor|nozzle_area')], - promotes_outputs=[('vec', 'motor_duct_area_nozzle_in')]) - - - hx_design_vars = ['ac|propulsion|thermal|hx|n_wide_cold', 'ac|propulsion|thermal|hx|n_long_cold', 'ac|propulsion|thermal|hx|n_tall'] - - #===========MOTOR LOOP======================= - - self.add_subsystem('motorheatsink', - LiquidCooledMotor(num_nodes=nn, case_cooling_coefficient=2100., - quasi_steady=False), - promotes_inputs=[('power_rating','ac|propulsion|motor|rating')]) - self.connect('hybrid_motor.heat_out','motorheatsink.q_in') - self.connect('hybrid_motor.component_weight','motorheatsink.motor_weight') - - self.add_subsystem('motorfaultprot', MotorFaultProtection(num_nodes=nn)) - self.connect('hybrid_motor.elec_load','motorfaultprot.motor_power') - - self.add_subsystem('hx_motor',HXGroup(num_nodes=nn),promotes_inputs=[(a, a.replace('hx','hx_motor')) for a in hx_design_vars]) - self.connect('rho_coolant','hx_motor.rho_hot') - fault_prot_promotes = [('ac|propulsion|thermal|hx|n_wide_cold', 'ac|propulsion|thermal|hx_motor|n_wide_cold'), - ('ac|propulsion|thermal|hx|n_long_cold', 'ac|propulsion|thermal|hx_fault_prot|n_long_cold'), - ('ac|propulsion|thermal|hx|n_tall', 'ac|propulsion|thermal|hx_motor|n_tall')] - self.add_subsystem('hx_fault_prot',HXGroup(num_nodes=nn),promotes_inputs=fault_prot_promotes) - self.connect('rho_coolant',['hx_fault_prot.rho_hot','motor_hose.rho_coolant','motor_coolant_pump.rho_coolant']) - - self.add_subsystem('motor_duct', - ImplicitCompressibleDuct_ExternalHX(num_nodes=nn,cfg=0.90), - promotes_inputs=[('p_inf','fltcond|p'),('T_inf','fltcond|T'),('Utrue','fltcond|Utrue'), - ('area_nozzle_in','motor_duct_area_nozzle_in')]) - - self.add_subsystem('motor_hose', SimpleHose(num_nodes=nn), promotes_inputs=[('hose_length', 'ac|geom|thermal|hx_to_motor_length'), - ('hose_diameter', 'ac|geom|thermal|hx_to_motor_diameter')]) - self.add_subsystem('motor_coolant_pump', SimplePump(num_nodes=nn), promotes_inputs=[('power_rating','ac|propulsion|thermal|hx_motor|pump_power_rating')]) - self.promote_add(sources=['motor_hose.delta_p','hx_motor.delta_p_hot'], prom_name='pressure_drop_motor_loop', factors=[1.0, -1.0], vec_size=nn, units='Pa') - self.connect('pressure_drop_motor_loop', 'motor_coolant_pump.delta_p') + thermal_params.add_output("mdot_coolant_fault_prot", val=0.19 * np.ones((nn,)), units="kg/s") + + thermal_params.add_output("bypass_heat_pump", val=np.ones((nn,))) + thermal_params.add_output("variable_duct_nozzle_area_start", val=20, units="inch**2") + thermal_params.add_output("variable_duct_nozzle_area_end", val=20, units="inch**2") + thermal_params.add_output("heat_pump_specific_power", val=200.0, units="W/kg") + thermal_params.add_output("heat_pump_eff_factor", val=0.4, units=None) + + self.add_subsystem( + "li_battery", + LinearInterpolator(num_nodes=nn, units="inch**2"), + promotes_outputs=[("vec", "variable_duct_nozzle_area")], + ) + self.connect("variable_duct_nozzle_area_start", "li_battery.start_val") + self.connect("variable_duct_nozzle_area_end", "li_battery.end_val") + self.add_subsystem( + "li_motor", + LinearInterpolator(num_nodes=nn, units="inch**2"), + promotes_inputs=[ + ("start_val", "ac|propulsion|thermal|hx_motor|nozzle_area"), + ("end_val", "ac|propulsion|thermal|hx_motor|nozzle_area"), + ], + promotes_outputs=[("vec", "motor_duct_area_nozzle_in")], + ) + + hx_design_vars = [ + "ac|propulsion|thermal|hx|n_wide_cold", + "ac|propulsion|thermal|hx|n_long_cold", + "ac|propulsion|thermal|hx|n_tall", + ] + + # ===========MOTOR LOOP======================= + + self.add_subsystem( + "motorheatsink", + LiquidCooledMotor(num_nodes=nn, case_cooling_coefficient=2100.0, quasi_steady=False), + promotes_inputs=[("power_rating", "ac|propulsion|motor|rating")], + ) + self.connect("hybrid_motor.heat_out", "motorheatsink.q_in") + self.connect("hybrid_motor.component_weight", "motorheatsink.motor_weight") + + self.add_subsystem("motorfaultprot", MotorFaultProtection(num_nodes=nn)) + self.connect("hybrid_motor.elec_load", "motorfaultprot.motor_power") + + self.add_subsystem( + "hx_motor", + HXGroup(num_nodes=nn), + promotes_inputs=[(a, a.replace("hx", "hx_motor")) for a in hx_design_vars], + ) + self.connect("rho_coolant", "hx_motor.rho_hot") + fault_prot_promotes = [ + ("ac|propulsion|thermal|hx|n_wide_cold", "ac|propulsion|thermal|hx_motor|n_wide_cold"), + ("ac|propulsion|thermal|hx|n_long_cold", "ac|propulsion|thermal|hx_fault_prot|n_long_cold"), + ("ac|propulsion|thermal|hx|n_tall", "ac|propulsion|thermal|hx_motor|n_tall"), + ] + self.add_subsystem("hx_fault_prot", HXGroup(num_nodes=nn), promotes_inputs=fault_prot_promotes) + self.connect( + "rho_coolant", ["hx_fault_prot.rho_hot", "motor_hose.rho_coolant", "motor_coolant_pump.rho_coolant"] + ) + + self.add_subsystem( + "motor_duct", + ImplicitCompressibleDuct_ExternalHX(num_nodes=nn, cfg=0.90), + promotes_inputs=[ + ("p_inf", "fltcond|p"), + ("T_inf", "fltcond|T"), + ("Utrue", "fltcond|Utrue"), + ("area_nozzle_in", "motor_duct_area_nozzle_in"), + ], + ) + + self.add_subsystem( + "motor_hose", + SimpleHose(num_nodes=nn), + promotes_inputs=[ + ("hose_length", "ac|geom|thermal|hx_to_motor_length"), + ("hose_diameter", "ac|geom|thermal|hx_to_motor_diameter"), + ], + ) + self.add_subsystem( + "motor_coolant_pump", + SimplePump(num_nodes=nn), + promotes_inputs=[("power_rating", "ac|propulsion|thermal|hx_motor|pump_power_rating")], + ) + self.promote_add( + sources=["motor_hose.delta_p", "hx_motor.delta_p_hot"], + prom_name="pressure_drop_motor_loop", + factors=[1.0, -1.0], + vec_size=nn, + units="Pa", + ) + self.connect("pressure_drop_motor_loop", "motor_coolant_pump.delta_p") # in to HXGroup: - self.connect('motor_duct.sta2.T', 'hx_fault_prot.T_in_cold') - self.connect('hx_fault_prot.T_out_cold', 'hx_motor.T_in_cold') - - self.connect('motor_duct.sta2.rho', ['hx_motor.rho_cold','hx_fault_prot.rho_cold']) - self.connect('motor_duct.mdot',['hx_motor.mdot_cold','hx_fault_prot.mdot_cold']) - self.connect('motorheatsink.T_out','hx_motor.T_in_hot') - self.connect('motorfaultprot.T_out','hx_fault_prot.T_in_hot') - self.connect('mdot_coolant_motor',['motorheatsink.mdot_coolant','hx_motor.mdot_hot','motor_hose.mdot_coolant','motor_coolant_pump.mdot_coolant']) - self.connect('mdot_coolant_fault_prot',['motorfaultprot.mdot_coolant','hx_fault_prot.mdot_hot']) - - #out from HXGroup - self.connect('hx_motor.frontal_area', ['motor_duct.area_2', 'motor_duct.area_3']) - self.connect('hx_motor.delta_p_cold', 'motorfaultprot.delta_p_motor_hx') - self.connect('hx_fault_prot.delta_p_cold', 'motorfaultprot.delta_p_fault_prot_hx') - self.connect('motorfaultprot.delta_p_stack','motor_duct.sta3.delta_p') - - self.connect('hx_motor.heat_transfer','motorfaultprot.heat_transfer_motor_hx') - self.connect('hx_fault_prot.heat_transfer','motorfaultprot.heat_transfer_fault_prot_hx') - self.connect('motorfaultprot.heat_transfer','motor_duct.sta3.heat_in') - - - self.connect('hx_motor.T_out_hot','motorheatsink.T_in') - self.connect('hx_fault_prot.T_out_hot','motorfaultprot.T_in') - - #=========BATTERY LOOP===================== - self.add_subsystem('batteryheatsink', - LiquidCooledBattery(num_nodes=nn, - quasi_steady=False), - promotes_inputs=[('battery_weight','ac|propulsion|battery|weight')]) - self.connect('battery.heat_out', 'batteryheatsink.q_in') - + self.connect("motor_duct.sta2.T", "hx_fault_prot.T_in_cold") + self.connect("hx_fault_prot.T_out_cold", "hx_motor.T_in_cold") + + self.connect("motor_duct.sta2.rho", ["hx_motor.rho_cold", "hx_fault_prot.rho_cold"]) + self.connect("motor_duct.mdot", ["hx_motor.mdot_cold", "hx_fault_prot.mdot_cold"]) + self.connect("motorheatsink.T_out", "hx_motor.T_in_hot") + self.connect("motorfaultprot.T_out", "hx_fault_prot.T_in_hot") + self.connect( + "mdot_coolant_motor", + [ + "motorheatsink.mdot_coolant", + "hx_motor.mdot_hot", + "motor_hose.mdot_coolant", + "motor_coolant_pump.mdot_coolant", + ], + ) + self.connect("mdot_coolant_fault_prot", ["motorfaultprot.mdot_coolant", "hx_fault_prot.mdot_hot"]) + + # out from HXGroup + self.connect("hx_motor.frontal_area", ["motor_duct.area_2", "motor_duct.area_3"]) + self.connect("hx_motor.delta_p_cold", "motorfaultprot.delta_p_motor_hx") + self.connect("hx_fault_prot.delta_p_cold", "motorfaultprot.delta_p_fault_prot_hx") + self.connect("motorfaultprot.delta_p_stack", "motor_duct.sta3.delta_p") + + self.connect("hx_motor.heat_transfer", "motorfaultprot.heat_transfer_motor_hx") + self.connect("hx_fault_prot.heat_transfer", "motorfaultprot.heat_transfer_fault_prot_hx") + self.connect("motorfaultprot.heat_transfer", "motor_duct.sta3.heat_in") + + self.connect("hx_motor.T_out_hot", "motorheatsink.T_in") + self.connect("hx_fault_prot.T_out_hot", "motorfaultprot.T_in") + + # =========BATTERY LOOP===================== + self.add_subsystem( + "batteryheatsink", + LiquidCooledBattery(num_nodes=nn, quasi_steady=False), + promotes_inputs=[("battery_weight", "ac|propulsion|battery|weight")], + ) + self.connect("battery.heat_out", "batteryheatsink.q_in") # self.connect('mdot_coolant_battery', ['batteryheatsink.mdot_coolant', 'hx_battery.mdot_hot', 'battery_hose.mdot_coolant', 'battery_coolant_pump.mdot_coolant']) - self.connect('mdot_coolant_battery', ['batteryheatsink.mdot_coolant', 'refrig.mdot_coolant', 'hx_battery.mdot_hot', 'battery_hose.mdot_coolant', 'battery_coolant_pump.mdot_coolant']) - - self.connect('batteryheatsink.T_out','refrig.T_in_cold') - self.connect('refrig.T_out_cold','batteryheatsink.T_in') - - self.add_subsystem('hx_battery',HXGroup(num_nodes=nn),promotes_inputs=hx_design_vars) - self.connect('rho_coolant',['hx_battery.rho_hot','battery_hose.rho_coolant', 'battery_coolant_pump.rho_coolant']) + self.connect( + "mdot_coolant_battery", + [ + "batteryheatsink.mdot_coolant", + "refrig.mdot_coolant", + "hx_battery.mdot_hot", + "battery_hose.mdot_coolant", + "battery_coolant_pump.mdot_coolant", + ], + ) + + self.connect("batteryheatsink.T_out", "refrig.T_in_cold") + self.connect("refrig.T_out_cold", "batteryheatsink.T_in") + + self.add_subsystem("hx_battery", HXGroup(num_nodes=nn), promotes_inputs=hx_design_vars) + self.connect( + "rho_coolant", ["hx_battery.rho_hot", "battery_hose.rho_coolant", "battery_coolant_pump.rho_coolant"] + ) # Hot side balance param will be set to the cooling duct nozzle area - self.add_subsystem('refrig', HeatPumpWithIntegratedCoolantLoop(num_nodes=nn), - promotes_inputs=[('power_rating','ac|propulsion|thermal|heatpump|power_rating')]) - self.connect('heat_pump_eff_factor','refrig.eff_factor') - self.connect('heat_pump_specific_power','refrig.specific_power') - - self.add_subsystem('variable_duct', - ImplicitCompressibleDuct_ExternalHX(num_nodes=nn, cfg=0.95), - promotes_inputs=[('p_inf','fltcond|p'),('T_inf','fltcond|T'),('Utrue','fltcond|Utrue')]) - self.connect('variable_duct_nozzle_area', 'variable_duct.area_nozzle_in') - - self.add_subsystem('battery_hose', SimpleHose(num_nodes=nn), promotes_inputs=[('hose_length', 'ac|geom|thermal|hx_to_battery_length'), - ('hose_diameter', 'ac|geom|thermal|hx_to_battery_diameter')]) - self.add_subsystem('battery_coolant_pump', SimplePump(num_nodes=nn), promotes_inputs=[('power_rating','ac|propulsion|thermal|hx|pump_power_rating')]) - self.promote_add(sources=['battery_hose.delta_p','hx_battery.delta_p_hot'], prom_name='pressure_drop_battery_loop', factors=[1.0, -1.0], vec_size=nn, units='Pa') - self.connect('pressure_drop_battery_loop', 'battery_coolant_pump.delta_p') + self.add_subsystem( + "refrig", + HeatPumpWithIntegratedCoolantLoop(num_nodes=nn), + promotes_inputs=[("power_rating", "ac|propulsion|thermal|heatpump|power_rating")], + ) + self.connect("heat_pump_eff_factor", "refrig.eff_factor") + self.connect("heat_pump_specific_power", "refrig.specific_power") + + self.add_subsystem( + "variable_duct", + ImplicitCompressibleDuct_ExternalHX(num_nodes=nn, cfg=0.95), + promotes_inputs=[("p_inf", "fltcond|p"), ("T_inf", "fltcond|T"), ("Utrue", "fltcond|Utrue")], + ) + self.connect("variable_duct_nozzle_area", "variable_duct.area_nozzle_in") + + self.add_subsystem( + "battery_hose", + SimpleHose(num_nodes=nn), + promotes_inputs=[ + ("hose_length", "ac|geom|thermal|hx_to_battery_length"), + ("hose_diameter", "ac|geom|thermal|hx_to_battery_diameter"), + ], + ) + self.add_subsystem( + "battery_coolant_pump", + SimplePump(num_nodes=nn), + promotes_inputs=[("power_rating", "ac|propulsion|thermal|hx|pump_power_rating")], + ) + self.promote_add( + sources=["battery_hose.delta_p", "hx_battery.delta_p_hot"], + prom_name="pressure_drop_battery_loop", + factors=[1.0, -1.0], + vec_size=nn, + units="Pa", + ) + self.connect("pressure_drop_battery_loop", "battery_coolant_pump.delta_p") # in to HXGroup: - self.connect('variable_duct.sta2.T', 'hx_battery.T_in_cold') - self.connect('variable_duct.sta2.rho', 'hx_battery.rho_cold') - self.connect('variable_duct.mdot','hx_battery.mdot_cold') + self.connect("variable_duct.sta2.T", "hx_battery.T_in_cold") + self.connect("variable_duct.sta2.rho", "hx_battery.rho_cold") + self.connect("variable_duct.mdot", "hx_battery.mdot_cold") - #out from HXGroup - self.connect('hx_battery.frontal_area', ['variable_duct.area_2', 'variable_duct.area_3']) - self.connect('hx_battery.delta_p_cold','variable_duct.sta3.delta_p') - self.connect('hx_battery.heat_transfer','variable_duct.sta3.heat_in') - self.connect('hx_battery.T_out_hot','refrig.T_in_hot') - self.connect('refrig.T_out_hot','hx_battery.T_in_hot') + # out from HXGroup + self.connect("hx_battery.frontal_area", ["variable_duct.area_2", "variable_duct.area_3"]) + self.connect("hx_battery.delta_p_cold", "variable_duct.sta3.delta_p") + self.connect("hx_battery.heat_transfer", "variable_duct.sta3.heat_in") + self.connect("hx_battery.T_out_hot", "refrig.T_in_hot") + self.connect("refrig.T_out_hot", "hx_battery.T_in_hot") # self.connect('hx_battery.T_out_hot', 'batteryheatsink.T_in') # self.connect('batteryheatsink.T_out', 'hx_battery.T_in_hot') - #=============WEIGHTS====================== + # =============WEIGHTS====================== # generally the weights module will be custom to each airplane # Motor, Battery, TMS, N+3 weight delta - self.promote_add(sources=['refrig.component_weight','hx_motor.component_weight','hx_battery.component_weight', - 'battery_hose.component_weight', 'battery_coolant_pump.component_weight', - 'motor_hose.component_weight', 'motor_coolant_pump.component_weight', - 'hx_fault_prot.component_weight', 'hybrid_motor.component_weight'], - promoted_sources=['ac|weights|OEW'], - prom_name='OEW', - factors=[2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 1.0], vec_size=1, units='kg') - - self.add_subsystem('weight', AddSubtractComp(output_name='weight', - input_names=['ac|design_mission|TOW', 'fuel_used'], - units='kg', vec_size=[1, nn], scaling_factors=[1, -1]), - promotes_inputs=['*'], - promotes_outputs=['weight']) - - self.promote_add(sources=[], - promoted_sources=['ac|design_mission|TOW', 'OEW', 'fuel_used_final', 'ac|propulsion|battery|weight'], - prom_name='margin', - factors=[1.0, -1.0, -1.0, -2.0], vec_size=1, units='kg') - - + self.promote_add( + sources=[ + "refrig.component_weight", + "hx_motor.component_weight", + "hx_battery.component_weight", + "battery_hose.component_weight", + "battery_coolant_pump.component_weight", + "motor_hose.component_weight", + "motor_coolant_pump.component_weight", + "hx_fault_prot.component_weight", + "hybrid_motor.component_weight", + ], + promoted_sources=["ac|weights|OEW"], + prom_name="OEW", + factors=[2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 1.0], + vec_size=1, + units="kg", + ) + + self.add_subsystem( + "weight", + AddSubtractComp( + output_name="weight", + input_names=["ac|design_mission|TOW", "fuel_used"], + units="kg", + vec_size=[1, nn], + scaling_factors=[1, -1], + ), + promotes_inputs=["*"], + promotes_outputs=["weight"], + ) + + self.promote_add( + sources=[], + promoted_sources=["ac|design_mission|TOW", "OEW", "fuel_used_final", "ac|propulsion|battery|weight"], + prom_name="margin", + factors=[1.0, -1.0, -1.0, -2.0], + vec_size=1, + units="kg", + ) + + class HybridSingleAisleAnalysisGroup(om.Group): """ Mission analysis group for N+3 hybrid single aisle """ + def setup(self): # Define number of analysis points to run pers mission segment nn = 21 # Define a bunch of design varaiables and airplane-specific parameters - dv_comp = self.add_subsystem('dv_comp', DictIndepVarComp(acdata), - promotes_outputs=["*"]) - dv_comp.add_output_from_dict('ac|aero|CLmax_TO') - dv_comp.add_output_from_dict('ac|aero|polar|e') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_TO') - dv_comp.add_output_from_dict('ac|aero|polar|CD0_cruise') - - dv_comp.add_output_from_dict('ac|geom|wing|S_ref') - dv_comp.add_output_from_dict('ac|geom|wing|AR') - dv_comp.add_output_from_dict('ac|geom|wing|c4sweep') - dv_comp.add_output_from_dict('ac|geom|wing|taper') - dv_comp.add_output_from_dict('ac|geom|wing|toverc') - dv_comp.add_output_from_dict('ac|geom|hstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|hstab|c4_to_wing_c4') - dv_comp.add_output_from_dict('ac|geom|vstab|S_ref') - dv_comp.add_output_from_dict('ac|geom|thermal|hx_to_battery_length') - dv_comp.add_output_from_dict('ac|geom|thermal|hx_to_battery_diameter') - dv_comp.add_output_from_dict('ac|geom|thermal|hx_to_motor_length') - dv_comp.add_output_from_dict('ac|geom|thermal|hx_to_motor_diameter') - - dv_comp.add_output_from_dict('ac|geom|nosegear|length') - dv_comp.add_output_from_dict('ac|geom|maingear|length') - - dv_comp.add_output_from_dict('ac|weights|MTOW') - dv_comp.add_output_from_dict('ac|weights|W_fuel_max') - dv_comp.add_output_from_dict('ac|weights|MLW') - dv_comp.add_output_from_dict('ac|weights|OEW') - - dv_comp.add_output_from_dict('ac|propulsion|engine|rating') - dv_comp.add_output_from_dict('ac|propulsion|motor|rating') - dv_comp.add_output_from_dict('ac|propulsion|battery|weight') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx|n_wide_cold') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx|n_long_cold') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx|n_tall') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx|pump_power_rating') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx_motor|pump_power_rating') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx_fault_prot|n_long_cold') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx_motor|n_wide_cold') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx_motor|n_long_cold') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx_motor|n_tall') - dv_comp.add_output_from_dict('ac|propulsion|thermal|hx_motor|nozzle_area') - dv_comp.add_output_from_dict('ac|propulsion|thermal|heatpump|power_rating') - - dv_comp.add_output_from_dict('ac|num_passengers_max') - dv_comp.add_output_from_dict('ac|q_cruise') - dv_comp.add_output_from_dict('ac|design_mission|TOW') + dv_comp = self.add_subsystem("dv_comp", DictIndepVarComp(acdata), promotes_outputs=["*"]) + dv_comp.add_output_from_dict("ac|aero|CLmax_TO") + dv_comp.add_output_from_dict("ac|aero|polar|e") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_TO") + dv_comp.add_output_from_dict("ac|aero|polar|CD0_cruise") + + dv_comp.add_output_from_dict("ac|geom|wing|S_ref") + dv_comp.add_output_from_dict("ac|geom|wing|AR") + dv_comp.add_output_from_dict("ac|geom|wing|c4sweep") + dv_comp.add_output_from_dict("ac|geom|wing|taper") + dv_comp.add_output_from_dict("ac|geom|wing|toverc") + dv_comp.add_output_from_dict("ac|geom|hstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|hstab|c4_to_wing_c4") + dv_comp.add_output_from_dict("ac|geom|vstab|S_ref") + dv_comp.add_output_from_dict("ac|geom|thermal|hx_to_battery_length") + dv_comp.add_output_from_dict("ac|geom|thermal|hx_to_battery_diameter") + dv_comp.add_output_from_dict("ac|geom|thermal|hx_to_motor_length") + dv_comp.add_output_from_dict("ac|geom|thermal|hx_to_motor_diameter") + + dv_comp.add_output_from_dict("ac|geom|nosegear|length") + dv_comp.add_output_from_dict("ac|geom|maingear|length") + + dv_comp.add_output_from_dict("ac|weights|MTOW") + dv_comp.add_output_from_dict("ac|weights|W_fuel_max") + dv_comp.add_output_from_dict("ac|weights|MLW") + dv_comp.add_output_from_dict("ac|weights|OEW") + + dv_comp.add_output_from_dict("ac|propulsion|engine|rating") + dv_comp.add_output_from_dict("ac|propulsion|motor|rating") + dv_comp.add_output_from_dict("ac|propulsion|battery|weight") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx|n_wide_cold") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx|n_long_cold") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx|n_tall") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx|pump_power_rating") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx_motor|pump_power_rating") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx_fault_prot|n_long_cold") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx_motor|n_wide_cold") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx_motor|n_long_cold") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx_motor|n_tall") + dv_comp.add_output_from_dict("ac|propulsion|thermal|hx_motor|nozzle_area") + dv_comp.add_output_from_dict("ac|propulsion|thermal|heatpump|power_rating") + + dv_comp.add_output_from_dict("ac|num_passengers_max") + dv_comp.add_output_from_dict("ac|q_cruise") + dv_comp.add_output_from_dict("ac|design_mission|TOW") # Run a full mission analysis including takeoff, reserve_, cruise,reserve_ and descereserve_nt - analysis = self.add_subsystem('analysis', - BasicMission(num_nodes=nn, - aircraft_model=HybridSingleAisleModel, - include_ground_roll=True), - promotes_inputs=['*'], promotes_outputs=['*']) - + analysis = self.add_subsystem( + "analysis", + BasicMission(num_nodes=nn, aircraft_model=HybridSingleAisleModel, include_ground_roll=True), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) def configure_problem(): prob = om.Problem() prob.model = HybridSingleAisleAnalysisGroup() - prob.model.nonlinear_solver = om.NewtonSolver(iprint=2,solve_subsystems=True) + prob.model.nonlinear_solver = om.NewtonSolver(iprint=2, solve_subsystems=True) prob.model.linear_solver = om.DirectSolver() - prob.model.nonlinear_solver.options['maxiter'] = 15 - prob.model.nonlinear_solver.options['atol'] = 5e-8 - prob.model.nonlinear_solver.options['rtol'] = 5e-8 - prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement='scalar', print_bound_enforce=False) + prob.model.nonlinear_solver.options["maxiter"] = 15 + prob.model.nonlinear_solver.options["atol"] = 5e-8 + prob.model.nonlinear_solver.options["rtol"] = 5e-8 + prob.model.nonlinear_solver.linesearch = om.BoundsEnforceLS(bound_enforcement="scalar", print_bound_enforce=False) return prob + def set_values(prob, num_nodes): # set some (required) mission parameters. Each pahse needs a vertical and air-speed # the entire mission needs a cruise altitude and range - prob.set_val('climb.fltcond|vs', np.linspace(2300., 600.,num_nodes), units='ft/min') - prob.set_val('climb.fltcond|Ueas', np.linspace(230, 220,num_nodes), units='kn') - prob.set_val('cruise.fltcond|vs', np.ones((num_nodes,)) * 4., units='ft/min') - prob.set_val('cruise.fltcond|Ueas', np.linspace(265, 258, num_nodes), units='kn') - prob.set_val('descent.fltcond|vs', np.linspace(-600, -150, num_nodes), units='ft/min') - prob.set_val('descent.fltcond|Ueas', np.ones((num_nodes,)) * 250, units='kn') - prob.set_val('cruise|h0',35000.,units='ft') - prob.set_val('mission_range',800,units='NM') - prob.set_val('takeoff|v2', 160., units='kn') - nozzleprs = [0.85,0.85,0.71,0.88] - phases_list = ['groundroll','climb', 'cruise', 'descent'] + prob.set_val("climb.fltcond|vs", np.linspace(2300.0, 600.0, num_nodes), units="ft/min") + prob.set_val("climb.fltcond|Ueas", np.linspace(230, 220, num_nodes), units="kn") + prob.set_val("cruise.fltcond|vs", np.ones((num_nodes,)) * 4.0, units="ft/min") + prob.set_val("cruise.fltcond|Ueas", np.linspace(265, 258, num_nodes), units="kn") + prob.set_val("descent.fltcond|vs", np.linspace(-600, -150, num_nodes), units="ft/min") + prob.set_val("descent.fltcond|Ueas", np.ones((num_nodes,)) * 250, units="kn") + prob.set_val("cruise|h0", 35000.0, units="ft") + prob.set_val("mission_range", 800, units="NM") + prob.set_val("takeoff|v2", 160.0, units="kn") + nozzleprs = [0.85, 0.85, 0.71, 0.88] + phases_list = ["groundroll", "climb", "cruise", "descent"] for i, phase in enumerate(phases_list): - prob.set_val(phase+'.hybrid_throttle_start', 0.00) - prob.set_val(phase+'.hybrid_throttle_end', 0.00) - prob.set_val(phase+'.fltcond|TempIncrement', 20, units='degC') - prob.set_val(phase+'.refrig.control.bypass_start', 1.0) - prob.set_val(phase+'.refrig.control.bypass_end', 1.0) - - for duct_name in ['variable_duct', 'motor_duct']: - prob.set_val(phase+'.'+duct_name+'.area_1', 150, units='inch**2') - prob.set_val(phase+'.'+duct_name+'.sta1.M', 0.8) - prob.set_val(phase+'.'+duct_name+'.sta2.M', 0.05) - prob.set_val(phase+'.'+duct_name+'.sta3.M', 0.05) - prob.set_val(phase+'.'+duct_name+'.nozzle.nozzle_pressure_ratio', 0.95) - prob.set_val(phase+'.'+duct_name+'.convergence_hack', -1.0, units='Pa') - prob.set_val(phase+'.hx_battery.channel_height_hot', 3, units='mm') - prob.set_val(phase+'.hx_motor.channel_height_hot', 3, units='mm') - prob.set_val(phase+'.hx_battery.cp_cold', 1002.93, units='J/kg/K') - prob.set_val(phase+'.hx_motor.cp_cold', 1002.93, units='J/kg/K') - for duct_name in ['variable_duct', 'motor_duct']: - prob.set_val('groundroll.'+duct_name+'.sta1.M', 0.2) - prob.set_val('groundroll.'+duct_name+'.nozzle.nozzle_pressure_ratio', 0.85) - prob.set_val('groundroll.'+duct_name+'.convergence_hack', -500, units='Pa') + prob.set_val(phase + ".hybrid_throttle_start", 0.00) + prob.set_val(phase + ".hybrid_throttle_end", 0.00) + prob.set_val(phase + ".fltcond|TempIncrement", 20, units="degC") + prob.set_val(phase + ".refrig.control.bypass_start", 1.0) + prob.set_val(phase + ".refrig.control.bypass_end", 1.0) + + for duct_name in ["variable_duct", "motor_duct"]: + prob.set_val(phase + "." + duct_name + ".area_1", 150, units="inch**2") + prob.set_val(phase + "." + duct_name + ".sta1.M", 0.8) + prob.set_val(phase + "." + duct_name + ".sta2.M", 0.05) + prob.set_val(phase + "." + duct_name + ".sta3.M", 0.05) + prob.set_val(phase + "." + duct_name + ".nozzle.nozzle_pressure_ratio", 0.95) + prob.set_val(phase + "." + duct_name + ".convergence_hack", -1.0, units="Pa") + prob.set_val(phase + ".hx_battery.channel_height_hot", 3, units="mm") + prob.set_val(phase + ".hx_motor.channel_height_hot", 3, units="mm") + prob.set_val(phase + ".hx_battery.cp_cold", 1002.93, units="J/kg/K") + prob.set_val(phase + ".hx_motor.cp_cold", 1002.93, units="J/kg/K") + for duct_name in ["variable_duct", "motor_duct"]: + prob.set_val("groundroll." + duct_name + ".sta1.M", 0.2) + prob.set_val("groundroll." + duct_name + ".nozzle.nozzle_pressure_ratio", 0.85) + prob.set_val("groundroll." + duct_name + ".convergence_hack", -500, units="Pa") # prob.set_val('groundroll.bypass_heat_pump', np.zeros((num_nodes,))) # prob.set_val('climb.bypass_heat_pump', np.zeros((num_nodes,))) - prob.set_val('groundroll.variable_duct_nozzle_area_start', 150, units='inch**2') - prob.set_val('groundroll.variable_duct_nozzle_area_end', 150, units='inch**2') - prob.set_val('descent.variable_duct_nozzle_area_start', 20, units='inch**2') - prob.set_val('descent.variable_duct_nozzle_area_end', 20, units='inch**2') + prob.set_val("groundroll.variable_duct_nozzle_area_start", 150, units="inch**2") + prob.set_val("groundroll.variable_duct_nozzle_area_end", 150, units="inch**2") + prob.set_val("descent.variable_duct_nozzle_area_start", 20, units="inch**2") + prob.set_val("descent.variable_duct_nozzle_area_end", 20, units="inch**2") + + prob.set_val("groundroll.hybrid_throttle_start", 1.0) + prob.set_val("groundroll.hybrid_throttle_end", 1.0) + prob.set_val("climb.hybrid_throttle_start", 1.0) + prob.set_val("climb.hybrid_throttle_end", 1.0) + prob.set_val("cruise.hybrid_throttle_start", 1.0) + prob.set_val("cruise.hybrid_throttle_end", 1.0) - prob.set_val('groundroll.hybrid_throttle_start', 1.0) - prob.set_val('groundroll.hybrid_throttle_end', 1.0) - prob.set_val('climb.hybrid_throttle_start', 1.0) - prob.set_val('climb.hybrid_throttle_end', 1.0) - prob.set_val('cruise.hybrid_throttle_start', 1.0) - prob.set_val('cruise.hybrid_throttle_end', 1.0) + prob.set_val("groundroll.motorheatsink.T_initial", 30.0, "degC") + prob.set_val("groundroll.batteryheatsink.T_initial", 30.0, "degC") + prob.set_val("groundroll.fltcond|Utrue", np.ones((num_nodes)) * 50, units="kn") - prob.set_val('groundroll.motorheatsink.T_initial', 30., 'degC') - prob.set_val('groundroll.batteryheatsink.T_initial', 30., 'degC') - prob.set_val('groundroll.fltcond|Utrue',np.ones((num_nodes))*50,units='kn') def show_outputs(prob): # print some outputs - vars_list = ['descent.fuel_used_final', 'descent.hx_battery.xs_area_cold', 'descent.hx_battery.frontal_area'] - units = ['lb','inch**2','inch**2'] - nice_print_names = ['Block fuel','Duct HX XS area','Duct HX Frontal Area'] + vars_list = ["descent.fuel_used_final", "descent.hx_battery.xs_area_cold", "descent.hx_battery.frontal_area"] + units = ["lb", "inch**2", "inch**2"] + nice_print_names = ["Block fuel", "Duct HX XS area", "Duct HX Frontal Area"] print("=======================================================================") for i, thing in enumerate(vars_list): - print(nice_print_names[i]+': '+str(prob.get_val(thing,units=units[i])[0])+' '+units[i]) + print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + units[i]) # plot some stuff plots = True if plots: - x_var = 'range' - x_unit = 'NM' - y_vars = ['fltcond|h','fltcond|Ueas','fuel_used','throttle','fltcond|vs','fltcond|M','fltcond|CL', - 'battery.SOC', 'motorheatsink.T', 'batteryheatsink.T', 'batteryheatsink.T_in', - 'variable_duct.force.F_net', 'motorheatsink.T_in', 'motor_duct.force.F_net','hx_fault_prot.T_out_hot'] - y_units = ['ft','kn','lbm',None,'ft/min', None, None, None, 'degC', 'degC','degC','lbf','degC','lbf','degC'] - x_label = 'Range (nmi)' - y_labels = ['Altitude (ft)', 'Veas airspeed (knots)', 'Fuel used (lb)', 'Throttle setting', - 'Vertical speed (ft/min)', 'Mach number', 'CL', 'Batt SOC', 'Motor Temp', 'Battery Temp (C)', - 'Battery Coolant Inflow Temp', 'Batt duct cooling Net Force (lb)', 'Motor Coolant Inflow Temp', - 'Motor duct cooling Net Force (lb)','Motor fault prot inflow temp (C)'] - phases = ['groundroll','climb', 'cruise', 'descent'] - plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, - x_label=x_label, y_labels=y_labels, marker='-', - plot_title='Hybrid Single Aisle Mission') + x_var = "range" + x_unit = "NM" + y_vars = [ + "fltcond|h", + "fltcond|Ueas", + "fuel_used", + "throttle", + "fltcond|vs", + "fltcond|M", + "fltcond|CL", + "battery.SOC", + "motorheatsink.T", + "batteryheatsink.T", + "batteryheatsink.T_in", + "variable_duct.force.F_net", + "motorheatsink.T_in", + "motor_duct.force.F_net", + "hx_fault_prot.T_out_hot", + ] + y_units = [ + "ft", + "kn", + "lbm", + None, + "ft/min", + None, + None, + None, + "degC", + "degC", + "degC", + "lbf", + "degC", + "lbf", + "degC", + ] + x_label = "Range (nmi)" + y_labels = [ + "Altitude (ft)", + "Veas airspeed (knots)", + "Fuel used (lb)", + "Throttle setting", + "Vertical speed (ft/min)", + "Mach number", + "CL", + "Batt SOC", + "Motor Temp", + "Battery Temp (C)", + "Battery Coolant Inflow Temp", + "Batt duct cooling Net Force (lb)", + "Motor Coolant Inflow Temp", + "Motor duct cooling Net Force (lb)", + "Motor fault prot inflow temp (C)", + ] + phases = ["groundroll", "climb", "cruise", "descent"] + plot_trajectory( + prob, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=x_label, + y_labels=y_labels, + marker="-", + plot_title="Hybrid Single Aisle Mission", + ) # prob.model.list_outputs() + def run_hybrid_sa_analysis(plots=True): num_nodes = 21 prob = configure_problem() - prob.setup(check=True, mode='fwd', force_alloc_complex=True) + prob.setup(check=True, mode="fwd", force_alloc_complex=True) set_values(prob, num_nodes) - phases_list = ['groundroll','climb', 'cruise', 'descent'] - print('=======================================') + phases_list = ["groundroll", "climb", "cruise", "descent"] + print("=======================================") for phase in phases_list: - if phase != 'groundroll': + if phase != "groundroll": # loss factor set per https://apps.dtic.mil/dtic/tr/fulltext/u2/002804.pdf for large area ratio diffuser - prob.set_val(phase+'.motor_duct.loss_factor_1', 0.20) - prob.set_val(phase+'.variable_duct.loss_factor_1', 0.20) - prob.set_val('cruise|h0',31000.,units='ft') - for phase in['climb','cruise','descent']: - prob.set_val(phase+'.refrig.control.bypass_start', 0.5) - prob.set_val(phase+'.refrig.control.bypass_end', 0.5) + prob.set_val(phase + ".motor_duct.loss_factor_1", 0.20) + prob.set_val(phase + ".variable_duct.loss_factor_1", 0.20) + prob.set_val("cruise|h0", 31000.0, units="ft") + for phase in ["climb", "cruise", "descent"]: + prob.set_val(phase + ".refrig.control.bypass_start", 0.5) + prob.set_val(phase + ".refrig.control.bypass_end", 0.5) prob.run_model() # set values and run the model in between to get it to converge - for phase in['climb','cruise','descent']: - prob.set_val(phase+'.refrig.control.bypass_start', 0.) # full refrigeration (1 is bypass refrigerator) - prob.set_val(phase+'.refrig.control.bypass_end', 0.) + for phase in ["climb", "cruise", "descent"]: + prob.set_val(phase + ".refrig.control.bypass_start", 0.0) # full refrigeration (1 is bypass refrigerator) + prob.set_val(phase + ".refrig.control.bypass_end", 0.0) prob.run_model() if plots: show_outputs(prob) - prob.cleanup() + prob.cleanup() return prob @@ -421,96 +615,121 @@ def run_hybrid_sa_optimization(plots=True): """ num_nodes = 21 prob = configure_problem() - prob.model.add_design_var('ac|design_mission|TOW', 50000, 79002, ref0=70000, ref=80000, units='kg') - prob.model.add_design_var('ac|propulsion|thermal|hx|n_wide_cold', 2, 1500, ref0=750, ref=1500, units=None) - prob.model.add_design_var('ac|propulsion|thermal|hx|n_long_cold', lower=3., upper=75., ref0=7, ref=75) - prob.model.add_design_var('ac|propulsion|thermal|hx_motor|n_wide_cold', 50, 1500, ref0=750, ref=1500, units=None) - prob.model.add_design_var('ac|propulsion|thermal|hx_motor|n_long_cold', lower=3., upper=75., ref0=7, ref=75) - prob.model.add_design_var('ac|propulsion|thermal|hx_motor|nozzle_area', lower=5., upper=60., ref0=5, ref=60) - prob.model.add_design_var('ac|propulsion|thermal|hx_motor|n_tall', lower=10., upper=25., ref0=5, ref=60) - prob.model.add_design_var('ac|propulsion|thermal|hx_fault_prot|n_long_cold', lower=1., upper=4., ref0=1, ref=4) - prob.model.add_design_var('climb.hybrid_throttle_start', lower=0.02, upper=1.0, ref0=0, ref=1) - prob.model.add_design_var('climb.hybrid_throttle_end', lower=0.02, upper=1.0, ref0=0, ref=1) - prob.model.add_design_var('cruise.hybrid_throttle_start', lower=0.02, upper=1.0, ref0=0, ref=1) - prob.model.add_design_var('cruise.hybrid_throttle_end', lower=0.02, upper=1.0, ref0=0, ref=1) - prob.model.add_design_var('descent.hybrid_throttle_start', lower=0.02, upper=0.3, ref0=0, ref=1) - prob.model.add_design_var('descent.hybrid_throttle_end', lower=0.02, upper=0.3, ref0=0, ref=1) - prob.model.add_design_var('ac|propulsion|battery|weight', lower=5000/2, upper=25000/2, ref0=2000/2, ref=15000/2) - prob.model.add_constraint('descent.battery.SOC_final', lower=0.05, ref0=0.05, ref=0.07) - prob.model.add_constraint('descent.hx_battery.width_overall', upper=1.2, ref=1.0) - prob.model.add_constraint('descent.hx_battery.xs_area_cold', lower=70, upper=300., units='inch**2', ref0=70, ref=100) - prob.model.add_constraint('descent.hx_motor.width_overall', upper=0.6, ref=1.0) - prob.model.add_constraint('descent.hx_motor.height_overall', upper=0.3, ref=1.0) - prob.model.add_constraint('descent.hx_motor.xs_area_cold', lower=70, upper=300., units='inch**2', ref0=70, ref=100) - prob.model.add_constraint('descent.battery_coolant_pump.component_sizing_margin', indices=[0], upper=1.0) - prob.model.add_constraint('descent.motor_coolant_pump.component_sizing_margin', indices=[0], upper=1.0) - prob.model.add_objective('descent.fuel_used_final', ref0=3800., ref=4200.) - prob.model.add_constraint('descent.margin', lower=20000, ref0=10000, ref=30000) - prob.model.add_design_var('ac|propulsion|thermal|heatpump|power_rating', lower=0.1, upper=50., units='kW', ref0=15., ref=50.) - prob.model.add_design_var('ac|propulsion|thermal|hx|pump_power_rating', lower=0.1, upper=5., units='kW', ref0=0., ref=5.) - prob.model.add_design_var('ac|geom|thermal|hx_to_battery_diameter', lower=0.5, upper=2., units='inch', ref0=0., ref=2.) - prob.model.add_design_var('ac|propulsion|thermal|hx_motor|pump_power_rating', lower=0.1, upper=5., units='kW', ref0=0., ref=5.) - prob.model.add_design_var('ac|geom|thermal|hx_to_motor_diameter', lower=0.5, upper=2., units='inch', ref0=0., ref=2.) - - for phase in ['climb','cruise','descent']: - prob.model.add_design_var(phase+'.refrig.control.bypass_start', lower=0.0, upper=1.0, units=None, ref=1.0) - prob.model.add_design_var(phase+'.refrig.control.bypass_end', lower=0.0, upper=1.0, units=None, ref=1.0) - - - for phase in ['groundroll']: - prob.model.add_design_var(phase+'.variable_duct_nozzle_area_start', lower=5., upper=150., ref0=148,ref=150, units='inch**2') - prob.model.add_design_var(phase+'.variable_duct_nozzle_area_end', lower=5., upper=150., ref0=148,ref=150, units='inch**2') - phases_list = ['climb','cruise'] + prob.model.add_design_var("ac|design_mission|TOW", 50000, 79002, ref0=70000, ref=80000, units="kg") + prob.model.add_design_var("ac|propulsion|thermal|hx|n_wide_cold", 2, 1500, ref0=750, ref=1500, units=None) + prob.model.add_design_var("ac|propulsion|thermal|hx|n_long_cold", lower=3.0, upper=75.0, ref0=7, ref=75) + prob.model.add_design_var("ac|propulsion|thermal|hx_motor|n_wide_cold", 50, 1500, ref0=750, ref=1500, units=None) + prob.model.add_design_var("ac|propulsion|thermal|hx_motor|n_long_cold", lower=3.0, upper=75.0, ref0=7, ref=75) + prob.model.add_design_var("ac|propulsion|thermal|hx_motor|nozzle_area", lower=5.0, upper=60.0, ref0=5, ref=60) + prob.model.add_design_var("ac|propulsion|thermal|hx_motor|n_tall", lower=10.0, upper=25.0, ref0=5, ref=60) + prob.model.add_design_var("ac|propulsion|thermal|hx_fault_prot|n_long_cold", lower=1.0, upper=4.0, ref0=1, ref=4) + prob.model.add_design_var("climb.hybrid_throttle_start", lower=0.02, upper=1.0, ref0=0, ref=1) + prob.model.add_design_var("climb.hybrid_throttle_end", lower=0.02, upper=1.0, ref0=0, ref=1) + prob.model.add_design_var("cruise.hybrid_throttle_start", lower=0.02, upper=1.0, ref0=0, ref=1) + prob.model.add_design_var("cruise.hybrid_throttle_end", lower=0.02, upper=1.0, ref0=0, ref=1) + prob.model.add_design_var("descent.hybrid_throttle_start", lower=0.02, upper=0.3, ref0=0, ref=1) + prob.model.add_design_var("descent.hybrid_throttle_end", lower=0.02, upper=0.3, ref0=0, ref=1) + prob.model.add_design_var( + "ac|propulsion|battery|weight", lower=5000 / 2, upper=25000 / 2, ref0=2000 / 2, ref=15000 / 2 + ) + prob.model.add_constraint("descent.battery.SOC_final", lower=0.05, ref0=0.05, ref=0.07) + prob.model.add_constraint("descent.hx_battery.width_overall", upper=1.2, ref=1.0) + prob.model.add_constraint( + "descent.hx_battery.xs_area_cold", lower=70, upper=300.0, units="inch**2", ref0=70, ref=100 + ) + prob.model.add_constraint("descent.hx_motor.width_overall", upper=0.6, ref=1.0) + prob.model.add_constraint("descent.hx_motor.height_overall", upper=0.3, ref=1.0) + prob.model.add_constraint("descent.hx_motor.xs_area_cold", lower=70, upper=300.0, units="inch**2", ref0=70, ref=100) + prob.model.add_constraint("descent.battery_coolant_pump.component_sizing_margin", indices=[0], upper=1.0) + prob.model.add_constraint("descent.motor_coolant_pump.component_sizing_margin", indices=[0], upper=1.0) + prob.model.add_objective("descent.fuel_used_final", ref0=3800.0, ref=4200.0) + prob.model.add_constraint("descent.margin", lower=20000, ref0=10000, ref=30000) + prob.model.add_design_var( + "ac|propulsion|thermal|heatpump|power_rating", lower=0.1, upper=50.0, units="kW", ref0=15.0, ref=50.0 + ) + prob.model.add_design_var( + "ac|propulsion|thermal|hx|pump_power_rating", lower=0.1, upper=5.0, units="kW", ref0=0.0, ref=5.0 + ) + prob.model.add_design_var( + "ac|geom|thermal|hx_to_battery_diameter", lower=0.5, upper=2.0, units="inch", ref0=0.0, ref=2.0 + ) + prob.model.add_design_var( + "ac|propulsion|thermal|hx_motor|pump_power_rating", lower=0.1, upper=5.0, units="kW", ref0=0.0, ref=5.0 + ) + prob.model.add_design_var( + "ac|geom|thermal|hx_to_motor_diameter", lower=0.5, upper=2.0, units="inch", ref0=0.0, ref=2.0 + ) + + for phase in ["climb", "cruise", "descent"]: + prob.model.add_design_var(phase + ".refrig.control.bypass_start", lower=0.0, upper=1.0, units=None, ref=1.0) + prob.model.add_design_var(phase + ".refrig.control.bypass_end", lower=0.0, upper=1.0, units=None, ref=1.0) + + for phase in ["groundroll"]: + prob.model.add_design_var( + phase + ".variable_duct_nozzle_area_start", lower=5.0, upper=150.0, ref0=148, ref=150, units="inch**2" + ) + prob.model.add_design_var( + phase + ".variable_duct_nozzle_area_end", lower=5.0, upper=150.0, ref0=148, ref=150, units="inch**2" + ) + phases_list = ["climb", "cruise"] for phase in phases_list: - prob.model.add_design_var(phase+'.variable_duct_nozzle_area_start', lower=5., upper=150., ref0=75,ref=150, units='inch**2') - prob.model.add_design_var(phase+'.variable_duct_nozzle_area_end', lower=5., upper=150., ref0=75,ref=150, units='inch**2') - prob.model.add_constraint(phase+'.batteryheatsink.T', upper=45, ref0=45, ref=50, units='degC') - prob.model.add_constraint(phase+'.motorheatsink.T', upper=90, ref0=45, ref=90, units='degC') - prob.model.add_constraint(phase+'.hx_fault_prot.T_out_hot', upper=50, ref0=45, ref=90, units='degC') - - phases_list = ['descent'] + prob.model.add_design_var( + phase + ".variable_duct_nozzle_area_start", lower=5.0, upper=150.0, ref0=75, ref=150, units="inch**2" + ) + prob.model.add_design_var( + phase + ".variable_duct_nozzle_area_end", lower=5.0, upper=150.0, ref0=75, ref=150, units="inch**2" + ) + prob.model.add_constraint(phase + ".batteryheatsink.T", upper=45, ref0=45, ref=50, units="degC") + prob.model.add_constraint(phase + ".motorheatsink.T", upper=90, ref0=45, ref=90, units="degC") + prob.model.add_constraint(phase + ".hx_fault_prot.T_out_hot", upper=50, ref0=45, ref=90, units="degC") + + phases_list = ["descent"] for phase in phases_list: - prob.model.add_design_var(phase+'.variable_duct_nozzle_area_start', lower=5., upper=150.,ref0=75,ref=150, units='inch**2') - prob.model.add_design_var(phase+'.variable_duct_nozzle_area_end', lower=5., upper=150., ref0=75,ref=150, units='inch**2') - constraintvals = np.ones((num_nodes,))*45 + prob.model.add_design_var( + phase + ".variable_duct_nozzle_area_start", lower=5.0, upper=150.0, ref0=75, ref=150, units="inch**2" + ) + prob.model.add_design_var( + phase + ".variable_duct_nozzle_area_end", lower=5.0, upper=150.0, ref0=75, ref=150, units="inch**2" + ) + constraintvals = np.ones((num_nodes,)) * 45 constraintvals[-1] = 35 - prob.model.add_constraint(phase+'.batteryheatsink.T', upper=constraintvals, ref0=35,ref=40, units='degC') - + prob.model.add_constraint(phase + ".batteryheatsink.T", upper=constraintvals, ref0=35, ref=40, units="degC") + prob.driver = om.ScipyOptimizeDriver() - prob.driver.options['optimizer'] = 'SLSQP' - prob.driver.opt_settings['limited_memory_max_history'] = 1000 - prob.driver.opt_settings['print_level'] = 1 - prob.driver.options['debug_print'] = ['objs']#,'desvars','nl_cons'] + prob.driver.options["optimizer"] = "SLSQP" + prob.driver.opt_settings["limited_memory_max_history"] = 1000 + prob.driver.opt_settings["print_level"] = 1 + prob.driver.options["debug_print"] = ["objs"] # ,'desvars','nl_cons'] - recorder = om.SqliteRecorder('HSA_Refrig_31kft.sql') + recorder = om.SqliteRecorder("HSA_Refrig_31kft.sql") prob.add_recorder(recorder) prob.driver.add_recorder(recorder) - prob.setup(check=True, mode='fwd', force_alloc_complex=True) + prob.setup(check=True, mode="fwd", force_alloc_complex=True) set_values(prob, num_nodes) - phases_list = ['groundroll','climb', 'cruise', 'descent'] - print('=======================================') + phases_list = ["groundroll", "climb", "cruise", "descent"] + print("=======================================") for phase in phases_list: - if phase != 'groundroll': + if phase != "groundroll": # loss factor set per https://apps.dtic.mil/dtic/tr/fulltext/u2/002804.pdf for large area ratio diffuser - prob.set_val(phase+'.motor_duct.loss_factor_1', 0.20) - prob.set_val(phase+'.variable_duct.loss_factor_1', 0.20) - prob.set_val('cruise|h0',31000.,units='ft') - for phase in['climb','cruise','descent']: - prob.set_val(phase+'.refrig.control.bypass_start', 0.5) - prob.set_val(phase+'.refrig.control.bypass_end', 0.5) + prob.set_val(phase + ".motor_duct.loss_factor_1", 0.20) + prob.set_val(phase + ".variable_duct.loss_factor_1", 0.20) + prob.set_val("cruise|h0", 31000.0, units="ft") + for phase in ["climb", "cruise", "descent"]: + prob.set_val(phase + ".refrig.control.bypass_start", 0.5) + prob.set_val(phase + ".refrig.control.bypass_end", 0.5) prob.run_model() # set values and run the model in between to get it to converge - for phase in['climb','cruise','descent']: - prob.set_val(phase+'.refrig.control.bypass_start', 0.) - prob.set_val(phase+'.refrig.control.bypass_end', 0.) + for phase in ["climb", "cruise", "descent"]: + prob.set_val(phase + ".refrig.control.bypass_start", 0.0) + prob.set_val(phase + ".refrig.control.bypass_end", 0.0) prob.run_driver() if plots: show_outputs(prob) - prob.cleanup() + prob.cleanup() return prob if __name__ == "__main__": - # run_hybrid_sa_analysis(plots=True) - run_hybrid_sa_optimization(plots=True) + # run_hybrid_sa_analysis(plots=True) + run_hybrid_sa_optimization(plots=True) diff --git a/openconcept/examples/aircraft_data/B738.py b/openconcept/examples/aircraft_data/B738.py index 341b999b..0e587706 100644 --- a/openconcept/examples/aircraft_data/B738.py +++ b/openconcept/examples/aircraft_data/B738.py @@ -6,63 +6,63 @@ ac = dict() # ==AERO================================== aero = dict() -aero['CLmax_TO'] = {'value' : 2.0} +aero["CLmax_TO"] = {"value": 2.0} polar = dict() -polar['e'] = {'value' : 0.801} -polar['CD0_TO'] = {'value' : 0.03} -polar['CD0_cruise'] = {'value' : 0.01925} +polar["e"] = {"value": 0.801} +polar["CD0_TO"] = {"value": 0.03} +polar["CD0_cruise"] = {"value": 0.01925} -aero['polar'] = polar -ac['aero'] = aero +aero["polar"] = polar +ac["aero"] = aero # ==GEOMETRY============================== geom = dict() wing = dict() -wing['S_ref'] = {'value': 124.6, 'units': 'm**2'} -wing['AR'] = {'value': 9.45} -wing['c4sweep'] = {'value': 25.0, 'units': 'deg'} -wing['taper'] = {'value': 0.159} -wing['toverc'] = {'value': 0.12} -geom['wing'] = wing +wing["S_ref"] = {"value": 124.6, "units": "m**2"} +wing["AR"] = {"value": 9.45} +wing["c4sweep"] = {"value": 25.0, "units": "deg"} +wing["taper"] = {"value": 0.159} +wing["toverc"] = {"value": 0.12} +geom["wing"] = wing hstab = dict() -hstab['S_ref'] = {'value': 32.78, 'units': 'm**2'} -hstab['c4_to_wing_c4'] = {'value': 17.9, 'units': 'm'} -geom['hstab'] = hstab +hstab["S_ref"] = {"value": 32.78, "units": "m**2"} +hstab["c4_to_wing_c4"] = {"value": 17.9, "units": "m"} +geom["hstab"] = hstab vstab = dict() -vstab['S_ref'] = {'value': 26.44, 'units': 'm**2'} -geom['vstab'] = vstab +vstab["S_ref"] = {"value": 26.44, "units": "m**2"} +geom["vstab"] = vstab nosegear = dict() -nosegear['length'] = {'value': 3, 'units': 'ft'} -geom['nosegear'] = nosegear +nosegear["length"] = {"value": 3, "units": "ft"} +geom["nosegear"] = nosegear maingear = dict() -maingear['length'] = {'value': 4, 'units': 'ft'} -geom['maingear'] = maingear +maingear["length"] = {"value": 4, "units": "ft"} +geom["maingear"] = maingear -ac['geom'] = geom +ac["geom"] = geom # ==WEIGHTS======================== weights = dict() -weights['MTOW'] = {'value': 79002, 'units': 'kg'} -weights['OEW'] = {'value': 0.530*79002, 'units': 'kg'} -weights['W_fuel_max'] = {'value': 0.266*79002, 'units': 'kg'} -weights['MLW'] = {'value': 66349, 'units': 'kg'} +weights["MTOW"] = {"value": 79002, "units": "kg"} +weights["OEW"] = {"value": 0.530 * 79002, "units": "kg"} +weights["W_fuel_max"] = {"value": 0.266 * 79002, "units": "kg"} +weights["MLW"] = {"value": 66349, "units": "kg"} -ac['weights'] = weights +ac["weights"] = weights # ==PROPULSION===================== propulsion = dict() engine = dict() -engine['rating'] = {'value': 27000, 'units': 'lbf'} -propulsion['engine'] = engine +engine["rating"] = {"value": 27000, "units": "lbf"} +propulsion["engine"] = engine -ac['propulsion'] = propulsion +ac["propulsion"] = propulsion # Some additional parameters needed by the empirical weights tools -ac['num_passengers_max'] = {'value': 180} -ac['q_cruise'] = {'value': 212.662, 'units': 'lb*ft**-2'} -data['ac'] = ac \ No newline at end of file +ac["num_passengers_max"] = {"value": 180} +ac["q_cruise"] = {"value": 212.662, "units": "lb*ft**-2"} +data["ac"] = ac diff --git a/openconcept/examples/aircraft_data/HybridSingleAisle.py b/openconcept/examples/aircraft_data/HybridSingleAisle.py index 80821332..89d20cc4 100644 --- a/openconcept/examples/aircraft_data/HybridSingleAisle.py +++ b/openconcept/examples/aircraft_data/HybridSingleAisle.py @@ -11,109 +11,110 @@ ac = dict() # ==AERO================================== aero = dict() -aero['CLmax_TO'] = {'value' : 2.0} +aero["CLmax_TO"] = {"value": 2.0} polar = dict() -polar['e'] = {'value' : 0.801} -polar['CD0_TO'] = {'value' : 0.03} -polar['CD0_cruise'] = {'value' : 0.01925} +polar["e"] = {"value": 0.801} +polar["CD0_TO"] = {"value": 0.03} +polar["CD0_cruise"] = {"value": 0.01925} -aero['polar'] = polar -ac['aero'] = aero +aero["polar"] = polar +ac["aero"] = aero # ==GEOMETRY============================== geom = dict() wing = dict() -wing['S_ref'] = {'value': 124.6, 'units': 'm**2'} -wing['AR'] = {'value': 9.45} -wing['c4sweep'] = {'value': 25.0, 'units': 'deg'} -wing['taper'] = {'value': 0.159} -wing['toverc'] = {'value': 0.12} -geom['wing'] = wing +wing["S_ref"] = {"value": 124.6, "units": "m**2"} +wing["AR"] = {"value": 9.45} +wing["c4sweep"] = {"value": 25.0, "units": "deg"} +wing["taper"] = {"value": 0.159} +wing["toverc"] = {"value": 0.12} +geom["wing"] = wing hstab = dict() -hstab['S_ref'] = {'value': 32.78, 'units': 'm**2'} -hstab['c4_to_wing_c4'] = {'value': 17.9, 'units': 'm'} -geom['hstab'] = hstab +hstab["S_ref"] = {"value": 32.78, "units": "m**2"} +hstab["c4_to_wing_c4"] = {"value": 17.9, "units": "m"} +geom["hstab"] = hstab vstab = dict() -vstab['S_ref'] = {'value': 26.44, 'units': 'm**2'} -geom['vstab'] = vstab +vstab["S_ref"] = {"value": 26.44, "units": "m**2"} +geom["vstab"] = vstab nosegear = dict() -nosegear['length'] = {'value': 3, 'units': 'ft'} -geom['nosegear'] = nosegear +nosegear["length"] = {"value": 3, "units": "ft"} +geom["nosegear"] = nosegear maingear = dict() -maingear['length'] = {'value': 4, 'units': 'ft'} -geom['maingear'] = maingear +maingear["length"] = {"value": 4, "units": "ft"} +geom["maingear"] = maingear thermal = dict() -thermal['hx_to_battery_length'] = {'value': 20, 'units': 'ft'} -thermal['hx_to_battery_diameter'] = {'value': 2, 'units': 'inch'} -thermal['hx_to_motor_length'] = {'value': 10, 'units': 'ft'} -thermal['hx_to_motor_diameter'] = {'value': 2, 'units':'inch'} -geom['thermal'] = thermal +thermal["hx_to_battery_length"] = {"value": 20, "units": "ft"} +thermal["hx_to_battery_diameter"] = {"value": 2, "units": "inch"} +thermal["hx_to_motor_length"] = {"value": 10, "units": "ft"} +thermal["hx_to_motor_diameter"] = {"value": 2, "units": "inch"} +geom["thermal"] = thermal -ac['geom'] = geom +ac["geom"] = geom # ==WEIGHTS======================== weights = dict() -weights['MTOW'] = {'value': 79002, 'units': 'kg'} -weights['OEW'] = {'value': 0.530*79002, 'units': 'kg'} -weights['W_fuel_max'] = {'value': 0.266*79002, 'units': 'kg'} -weights['MLW'] = {'value': 66349, 'units': 'kg'} +weights["MTOW"] = {"value": 79002, "units": "kg"} +weights["OEW"] = {"value": 0.530 * 79002, "units": "kg"} +weights["W_fuel_max"] = {"value": 0.266 * 79002, "units": "kg"} +weights["MLW"] = {"value": 66349, "units": "kg"} -ac['weights'] = weights +ac["weights"] = weights # ==PROPULSION===================== propulsion = dict() engine = dict() -engine['rating'] = {'value': 27000, 'units': 'lbf'} -propulsion['engine'] = engine +engine["rating"] = {"value": 27000, "units": "lbf"} +propulsion["engine"] = engine motor = dict() -motor['rating'] = {'value': 1.0, 'units': 'MW'} -propulsion['motor'] = motor +motor["rating"] = {"value": 1.0, "units": "MW"} +propulsion["motor"] = motor battery = dict() -battery['weight'] = {'value': 2000, 'units': 'kg'} -propulsion['battery'] = battery +battery["weight"] = {"value": 2000, "units": "kg"} +propulsion["battery"] = battery thermal = dict() hx = dict() -hx['n_wide_cold'] = {'value': 750, 'units': None} -hx['n_long_cold'] = {'value': 3, 'units': None} -hx['n_tall'] = {'value': 50, 'units': None} -hx['pump_power_rating'] = {'value': 5.0, 'units':'kW'} -thermal['hx'] = hx +hx["n_wide_cold"] = {"value": 750, "units": None} +hx["n_long_cold"] = {"value": 3, "units": None} +hx["n_tall"] = {"value": 50, "units": None} +hx["pump_power_rating"] = {"value": 5.0, "units": "kW"} +thermal["hx"] = hx hx_motor = dict() -hx_motor['n_wide_cold'] = {'value': 750, 'units': None} -hx_motor['n_long_cold'] = {'value': 3, 'units': None} -hx_motor['n_tall'] = {'value': 10, 'units': None} -hx_motor['nozzle_area'] = {'value': 40, 'units': 'inch**2'} -hx_motor['pump_power_rating'] = {'value': 5.0, 'units':'kW'} +hx_motor["n_wide_cold"] = {"value": 750, "units": None} +hx_motor["n_long_cold"] = {"value": 3, "units": None} +hx_motor["n_tall"] = {"value": 10, "units": None} +hx_motor["nozzle_area"] = {"value": 40, "units": "inch**2"} +hx_motor["pump_power_rating"] = {"value": 5.0, "units": "kW"} -thermal['hx_motor'] = hx_motor +thermal["hx_motor"] = hx_motor hx_fault_prot = dict() -hx_fault_prot['n_long_cold'] = {'value': 1.5, 'units': None} -thermal['hx_fault_prot'] = hx_fault_prot +hx_fault_prot["n_long_cold"] = {"value": 1.5, "units": None} +thermal["hx_fault_prot"] = hx_fault_prot heatpump = dict() -heatpump['power_rating'] = {'value': 30, 'units': 'kW'} -thermal['heatpump'] = heatpump -propulsion['thermal'] = thermal +heatpump["power_rating"] = {"value": 30, "units": "kW"} +thermal["heatpump"] = heatpump +propulsion["thermal"] = thermal -ac['propulsion'] = propulsion +ac["propulsion"] = propulsion # Some additional parameters needed by the empirical weights tools -ac['num_passengers_max'] = {'value': 180} -ac['q_cruise'] = {'value': 212.662, 'units': 'lb*ft**-2'} +ac["num_passengers_max"] = {"value": 180} +ac["q_cruise"] = {"value": 212.662, "units": "lb*ft**-2"} design_mission = dict() -design_mission['TOW'] = {'value':79002, 'units':'kg'} -ac['design_mission'] = design_mission -data['ac'] = ac +design_mission["TOW"] = {"value": 79002, "units": "kg"} +ac["design_mission"] = design_mission +data["ac"] = ac + class MotorFaultProtection(IntegratorGroup): """ @@ -121,15 +122,39 @@ class MotorFaultProtection(IntegratorGroup): It consumes glycol/water at 3gpm and needs 40C inflow temp So it has to be stacked first in the motor HX duct unfortunately """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('efficiency', default=0.995) - + self.options.declare("num_nodes", default=1) + self.options.declare("efficiency", default=0.995) + def setup(self): - nn = self.options['num_nodes'] - eff = self.options['efficiency'] - self.add_subsystem('coolant_temps', PerfectHeatTransferComp(num_nodes=nn), promotes=['T_in','mdot_coolant','T_out']) - self.add_subsystem('electrical_loss', ElementMultiplyDivideComp('elec_load', input_names=['motor_power'], vec_size=nn, scaling_factor=(1-eff), input_units=['W']), promotes=['*']) - self.connect('elec_load', 'coolant_temps.q') - self.add_subsystem('duct_pressure', AddSubtractComp('delta_p_stack', input_names=['delta_p_motor_hx','delta_p_fault_prot_hx'], vec_size=nn, units='Pa'), promotes=['*']) - self.add_subsystem('heat_transfer', AddSubtractComp('heat_transfer', input_names=['heat_transfer_motor_hx','heat_transfer_fault_prot_hx'], vec_size=nn, units='W'), promotes=['*']) + nn = self.options["num_nodes"] + eff = self.options["efficiency"] + self.add_subsystem( + "coolant_temps", PerfectHeatTransferComp(num_nodes=nn), promotes=["T_in", "mdot_coolant", "T_out"] + ) + self.add_subsystem( + "electrical_loss", + ElementMultiplyDivideComp( + "elec_load", input_names=["motor_power"], vec_size=nn, scaling_factor=(1 - eff), input_units=["W"] + ), + promotes=["*"], + ) + self.connect("elec_load", "coolant_temps.q") + self.add_subsystem( + "duct_pressure", + AddSubtractComp( + "delta_p_stack", input_names=["delta_p_motor_hx", "delta_p_fault_prot_hx"], vec_size=nn, units="Pa" + ), + promotes=["*"], + ) + self.add_subsystem( + "heat_transfer", + AddSubtractComp( + "heat_transfer", + input_names=["heat_transfer_motor_hx", "heat_transfer_fault_prot_hx"], + vec_size=nn, + units="W", + ), + promotes=["*"], + ) diff --git a/openconcept/examples/aircraft_data/KingAirC90GT.py b/openconcept/examples/aircraft_data/KingAirC90GT.py index d6260dab..b0d04399 100644 --- a/openconcept/examples/aircraft_data/KingAirC90GT.py +++ b/openconcept/examples/aircraft_data/KingAirC90GT.py @@ -6,83 +6,83 @@ ac = dict() # ==AERO================================== aero = dict() -aero['CLmax_TO'] = {'value' : 1.52} +aero["CLmax_TO"] = {"value": 1.52} polar = dict() -polar['e'] = {'value' : 0.80} -polar['CD0_TO'] = {'value' : 0.040} -polar['CD0_cruise'] = {'value' : 0.022} +polar["e"] = {"value": 0.80} +polar["CD0_TO"] = {"value": 0.040} +polar["CD0_cruise"] = {"value": 0.022} -aero['polar'] = polar -ac['aero'] = aero +aero["polar"] = polar +ac["aero"] = aero # ==GEOMETRY============================== geom = dict() wing = dict() -wing['S_ref'] = {'value': 27.308, 'units': 'm**2'} -wing['AR'] = {'value': 8.5834} -wing['c4sweep'] = {'value': 1.0, 'units': 'deg'} -wing['taper'] = {'value': 0.397} -wing['toverc'] = {'value': 0.19} -geom['wing'] = wing +wing["S_ref"] = {"value": 27.308, "units": "m**2"} +wing["AR"] = {"value": 8.5834} +wing["c4sweep"] = {"value": 1.0, "units": "deg"} +wing["taper"] = {"value": 0.397} +wing["toverc"] = {"value": 0.19} +geom["wing"] = wing fuselage = dict() -fuselage['S_wet'] = {'value': 41.3, 'units': 'm**2'} -fuselage['width'] = {'value': 1.6, 'units': 'm'} -fuselage['length'] = {'value': 10.79, 'units': 'm'} -fuselage['height'] = {'value': 1.9, 'units': 'm'} -geom['fuselage'] = fuselage +fuselage["S_wet"] = {"value": 41.3, "units": "m**2"} +fuselage["width"] = {"value": 1.6, "units": "m"} +fuselage["length"] = {"value": 10.79, "units": "m"} +fuselage["height"] = {"value": 1.9, "units": "m"} +geom["fuselage"] = fuselage hstab = dict() -hstab['S_ref'] = {'value': 8.08, 'units': 'm**2'} -hstab['c4_to_wing_c4'] = {'value': 5.33, 'units': 'm'} -geom['hstab'] = hstab +hstab["S_ref"] = {"value": 8.08, "units": "m**2"} +hstab["c4_to_wing_c4"] = {"value": 5.33, "units": "m"} +geom["hstab"] = hstab vstab = dict() -vstab['S_ref'] = {'value': 3.4, 'units': 'm**2'} -geom['vstab'] = vstab +vstab["S_ref"] = {"value": 3.4, "units": "m**2"} +geom["vstab"] = vstab nosegear = dict() -nosegear['length'] = {'value': 0.95, 'units': 'm'} -geom['nosegear'] = nosegear +nosegear["length"] = {"value": 0.95, "units": "m"} +geom["nosegear"] = nosegear maingear = dict() -maingear['length'] = {'value': 0.88, 'units': 'm'} -geom['maingear'] = maingear +maingear["length"] = {"value": 0.88, "units": "m"} +geom["maingear"] = maingear -ac['geom'] = geom +ac["geom"] = geom # ==WEIGHTS======================== weights = dict() -weights['MTOW'] = {'value': 4581, 'units': 'kg'} -weights['W_fuel_max'] = {'value': 1166, 'units': 'kg'} -weights['MLW'] = {'value': 4355, 'units': 'kg'} -weights['W_battery'] = {'value': 100, 'units': 'kg'} +weights["MTOW"] = {"value": 4581, "units": "kg"} +weights["W_fuel_max"] = {"value": 1166, "units": "kg"} +weights["MLW"] = {"value": 4355, "units": "kg"} +weights["W_battery"] = {"value": 100, "units": "kg"} -ac['weights'] = weights +ac["weights"] = weights # ==PROPULSION===================== propulsion = dict() engine = dict() -engine['rating'] = {'value': 750, 'units': 'hp'} -propulsion['engine'] = engine +engine["rating"] = {"value": 750, "units": "hp"} +propulsion["engine"] = engine propeller = dict() -propeller['diameter'] = {'value': 2.28, 'units': 'm'} -propulsion['propeller'] = propeller +propeller["diameter"] = {"value": 2.28, "units": "m"} +propulsion["propeller"] = propeller motor = dict() -motor['rating'] = {'value': 527.2, 'units': 'hp'} -propulsion['motor'] = motor +motor["rating"] = {"value": 527.2, "units": "hp"} +propulsion["motor"] = motor generator = dict() -generator['rating'] = {'value': 1083.7, 'units': 'hp'} -propulsion['generator'] = generator +generator["rating"] = {"value": 1083.7, "units": "hp"} +propulsion["generator"] = generator -ac['propulsion'] = propulsion +ac["propulsion"] = propulsion # Some additional parameters needed by the empirical weights tools -ac['num_passengers_max'] = {'value': 8} -ac['q_cruise'] = {'value': 98, 'units': 'lb*ft**-2'} -ac['num_engines'] = {'value': 2} -data['ac'] = ac \ No newline at end of file +ac["num_passengers_max"] = {"value": 8} +ac["q_cruise"] = {"value": 98, "units": "lb*ft**-2"} +ac["num_engines"] = {"value": 2} +data["ac"] = ac diff --git a/openconcept/examples/aircraft_data/TBM850.py b/openconcept/examples/aircraft_data/TBM850.py index 736d81ac..c281c9b0 100644 --- a/openconcept/examples/aircraft_data/TBM850.py +++ b/openconcept/examples/aircraft_data/TBM850.py @@ -6,73 +6,73 @@ ac = dict() # ==AERO================================== aero = dict() -aero['CLmax_TO'] = {'value' : 1.7} +aero["CLmax_TO"] = {"value": 1.7} polar = dict() -polar['e'] = {'value' : 0.78} -polar['CD0_TO'] = {'value' : 0.03} -polar['CD0_cruise'] = {'value' : 0.0205} +polar["e"] = {"value": 0.78} +polar["CD0_TO"] = {"value": 0.03} +polar["CD0_cruise"] = {"value": 0.0205} -aero['polar'] = polar -ac['aero'] = aero +aero["polar"] = polar +ac["aero"] = aero # ==GEOMETRY============================== geom = dict() wing = dict() -wing['S_ref'] = {'value': 18.0, 'units': 'm**2'} -wing['AR'] = {'value': 8.95} -wing['c4sweep'] = {'value': 1.0, 'units': 'deg'} -wing['taper'] = {'value': 0.622} -wing['toverc'] = {'value': 0.16} -geom['wing'] = wing +wing["S_ref"] = {"value": 18.0, "units": "m**2"} +wing["AR"] = {"value": 8.95} +wing["c4sweep"] = {"value": 1.0, "units": "deg"} +wing["taper"] = {"value": 0.622} +wing["toverc"] = {"value": 0.16} +geom["wing"] = wing fuselage = dict() -fuselage['S_wet'] = {'value': 392, 'units': 'ft**2'} -fuselage['width'] = {'value': 4.58, 'units': 'ft'} -fuselage['length'] = {'value': 27.39, 'units': 'ft'} -fuselage['height'] = {'value': 5.555, 'units': 'ft'} -geom['fuselage'] = fuselage +fuselage["S_wet"] = {"value": 392, "units": "ft**2"} +fuselage["width"] = {"value": 4.58, "units": "ft"} +fuselage["length"] = {"value": 27.39, "units": "ft"} +fuselage["height"] = {"value": 5.555, "units": "ft"} +geom["fuselage"] = fuselage hstab = dict() -hstab['S_ref'] = {'value': 47.5, 'units': 'ft**2'} -hstab['c4_to_wing_c4'] = {'value': 17.9, 'units': 'ft'} -geom['hstab'] = hstab +hstab["S_ref"] = {"value": 47.5, "units": "ft**2"} +hstab["c4_to_wing_c4"] = {"value": 17.9, "units": "ft"} +geom["hstab"] = hstab vstab = dict() -vstab['S_ref'] = {'value': 31.36, 'units': 'ft**2'} -geom['vstab'] = vstab +vstab["S_ref"] = {"value": 31.36, "units": "ft**2"} +geom["vstab"] = vstab nosegear = dict() -nosegear['length'] = {'value': 3, 'units': 'ft'} -geom['nosegear'] = nosegear +nosegear["length"] = {"value": 3, "units": "ft"} +geom["nosegear"] = nosegear maingear = dict() -maingear['length'] = {'value': 4, 'units': 'ft'} -geom['maingear'] = maingear +maingear["length"] = {"value": 4, "units": "ft"} +geom["maingear"] = maingear -ac['geom'] = geom +ac["geom"] = geom # ==WEIGHTS======================== weights = dict() -weights['MTOW'] = {'value': 3353, 'units': 'kg'} -weights['W_fuel_max'] = {'value': 2000, 'units': 'lb'} -weights['MLW'] = {'value': 7000, 'units': 'lb'} +weights["MTOW"] = {"value": 3353, "units": "kg"} +weights["W_fuel_max"] = {"value": 2000, "units": "lb"} +weights["MLW"] = {"value": 7000, "units": "lb"} -ac['weights'] = weights +ac["weights"] = weights # ==PROPULSION===================== propulsion = dict() engine = dict() -engine['rating'] = {'value': 850, 'units': 'hp'} -propulsion['engine'] = engine +engine["rating"] = {"value": 850, "units": "hp"} +propulsion["engine"] = engine propeller = dict() -propeller['diameter'] = {'value': 2.31, 'units': 'm'} -propulsion['propeller'] = propeller +propeller["diameter"] = {"value": 2.31, "units": "m"} +propulsion["propeller"] = propeller -ac['propulsion'] = propulsion +ac["propulsion"] = propulsion # Some additional parameters needed by the empirical weights tools -ac['num_passengers_max'] = {'value': 6} -ac['q_cruise'] = {'value': 135.4, 'units': 'lb*ft**-2'} -data['ac'] = ac \ No newline at end of file +ac["num_passengers_max"] = {"value": 6} +ac["q_cruise"] = {"value": 135.4, "units": "lb*ft**-2"} +data["ac"] = ac diff --git a/openconcept/examples/aircraft_data/caravan.py b/openconcept/examples/aircraft_data/caravan.py index 8168e751..eb8a8844 100644 --- a/openconcept/examples/aircraft_data/caravan.py +++ b/openconcept/examples/aircraft_data/caravan.py @@ -6,73 +6,73 @@ ac = dict() # ==AERO================================== aero = dict() -aero['CLmax_TO'] = {'value' : 2.25} +aero["CLmax_TO"] = {"value": 2.25} polar = dict() -polar['e'] = {'value' : 0.8} -polar['CD0_TO'] = {'value' : 0.033} -polar['CD0_cruise'] = {'value' : 0.027} +polar["e"] = {"value": 0.8} +polar["CD0_TO"] = {"value": 0.033} +polar["CD0_cruise"] = {"value": 0.027} -aero['polar'] = polar -ac['aero'] = aero +aero["polar"] = polar +ac["aero"] = aero # ==GEOMETRY============================== geom = dict() wing = dict() -wing['S_ref'] = {'value': 26.0, 'units': 'm**2'} -wing['AR'] = {'value': 9.69} -wing['c4sweep'] = {'value': 1.0, 'units': 'deg'} -wing['taper'] = {'value': 0.625} -wing['toverc'] = {'value': 0.19} -geom['wing'] = wing +wing["S_ref"] = {"value": 26.0, "units": "m**2"} +wing["AR"] = {"value": 9.69} +wing["c4sweep"] = {"value": 1.0, "units": "deg"} +wing["taper"] = {"value": 0.625} +wing["toverc"] = {"value": 0.19} +geom["wing"] = wing fuselage = dict() -fuselage['S_wet'] = {'value': 490, 'units': 'ft**2'} -fuselage['width'] = {'value': 1.7, 'units': 'm'} -fuselage['length'] = {'value': 12.67, 'units': 'm'} -fuselage['height'] = {'value': 1.73, 'units': 'm'} -geom['fuselage'] = fuselage +fuselage["S_wet"] = {"value": 490, "units": "ft**2"} +fuselage["width"] = {"value": 1.7, "units": "m"} +fuselage["length"] = {"value": 12.67, "units": "m"} +fuselage["height"] = {"value": 1.73, "units": "m"} +geom["fuselage"] = fuselage hstab = dict() -hstab['S_ref'] = {'value': 6.93, 'units': 'm**2'} -hstab['c4_to_wing_c4'] = {'value': 7.28, 'units': 'm'} -geom['hstab'] = hstab +hstab["S_ref"] = {"value": 6.93, "units": "m**2"} +hstab["c4_to_wing_c4"] = {"value": 7.28, "units": "m"} +geom["hstab"] = hstab vstab = dict() -vstab['S_ref'] = {'value': 3.34, 'units': 'm**2'} -geom['vstab'] = vstab +vstab["S_ref"] = {"value": 3.34, "units": "m**2"} +geom["vstab"] = vstab nosegear = dict() -nosegear['length'] = {'value': 0.9, 'units': 'm'} -geom['nosegear'] = nosegear +nosegear["length"] = {"value": 0.9, "units": "m"} +geom["nosegear"] = nosegear maingear = dict() -maingear['length'] = {'value': 0.92, 'units': 'm'} -geom['maingear'] = maingear +maingear["length"] = {"value": 0.92, "units": "m"} +geom["maingear"] = maingear -ac['geom'] = geom +ac["geom"] = geom # ==WEIGHTS======================== weights = dict() -weights['MTOW'] = {'value': 3970, 'units': 'kg'} -weights['W_fuel_max'] = {'value': 1018, 'units': 'kg'} -weights['MLW'] = {'value': 3358, 'units': 'kg'} +weights["MTOW"] = {"value": 3970, "units": "kg"} +weights["W_fuel_max"] = {"value": 1018, "units": "kg"} +weights["MLW"] = {"value": 3358, "units": "kg"} -ac['weights'] = weights +ac["weights"] = weights # ==PROPULSION===================== propulsion = dict() engine = dict() -engine['rating'] = {'value': 675, 'units': 'hp'} -propulsion['engine'] = engine +engine["rating"] = {"value": 675, "units": "hp"} +propulsion["engine"] = engine propeller = dict() -propeller['diameter'] = {'value': 2.1, 'units': 'm'} -propulsion['propeller'] = propeller +propeller["diameter"] = {"value": 2.1, "units": "m"} +propulsion["propeller"] = propeller -ac['propulsion'] = propulsion +ac["propulsion"] = propulsion # Some additional parameters needed by the empirical weights tools -ac['num_passengers_max'] = {'value': 2} -ac['q_cruise'] = {'value': 56.9621, 'units': 'lb*ft**-2'} -data['ac'] = ac \ No newline at end of file +ac["num_passengers_max"] = {"value": 2} +ac["q_cruise"] = {"value": 56.9621, "units": "lb*ft**-2"} +data["ac"] = ac diff --git a/openconcept/examples/minimal.py b/openconcept/examples/minimal.py index 9fed8d7f..0e16e6fb 100644 --- a/openconcept/examples/minimal.py +++ b/openconcept/examples/minimal.py @@ -123,11 +123,14 @@ def setup_problem(model=MissionAnalysis): if __name__ == "__main__": # Process command line argument to optionally not show figures and N2 diagram import argparse + parser = argparse.ArgumentParser() - parser.add_argument("--hide_visuals", - default=False, - action="store_true", - help="Do not show matplotlib figure or open N2 diagram in browser") + parser.add_argument( + "--hide_visuals", + default=False, + action="store_true", + help="Do not show matplotlib figure or open N2 diagram in browser", + ) hide_viz = parser.parse_args().hide_visuals # Setup the problem and run the analysis @@ -160,7 +163,7 @@ def setup_problem(model=MissionAnalysis): prob.get_val(f"mission.{phase}.{var['var']}", units=var["units"]), "-o", c="tab:blue", - markersize=2., + markersize=2.0, ) fig.savefig("minimal_example_results.svg", transparent=True) diff --git a/openconcept/examples/minimal_integrator.py b/openconcept/examples/minimal_integrator.py index c662be7c..abec2e15 100644 --- a/openconcept/examples/minimal_integrator.py +++ b/openconcept/examples/minimal_integrator.py @@ -64,7 +64,9 @@ def setup(self): # Integrate the fuel flow rate to compute fuel burn # rst Integrator (beg) - integ = self.add_subsystem("fuel_integrator", Integrator(num_nodes=nn, diff_units="s", time_setup="duration", method="simpson")) + integ = self.add_subsystem( + "fuel_integrator", Integrator(num_nodes=nn, diff_units="s", time_setup="duration", method="simpson") + ) integ.add_integrand("fuel_burned", rate_name="fuel_flow", units="kg") self.connect("fuel_flow_calc.fuel_flow", "fuel_integrator.fuel_flow") @@ -120,11 +122,14 @@ def setup(self): if __name__ == "__main__": # Process command line argument to optionally not show figures and N2 diagram import argparse + parser = argparse.ArgumentParser() - parser.add_argument("--hide_visuals", - default=False, - action="store_true", - help="Do not show matplotlib figure or open N2 diagram in browser") + parser.add_argument( + "--hide_visuals", + default=False, + action="store_true", + help="Do not show matplotlib figure or open N2 diagram in browser", + ) hide_viz = parser.parse_args().hide_visuals # Setup the problem and run the analysis diff --git a/openconcept/examples/tests/test_example_aircraft.py b/openconcept/examples/tests/test_example_aircraft.py index 9245e318..f3cb6765 100644 --- a/openconcept/examples/tests/test_example_aircraft.py +++ b/openconcept/examples/tests/test_example_aircraft.py @@ -1,4 +1,3 @@ - import unittest import numpy as np from openmdao.utils.assert_utils import assert_near_equal @@ -13,10 +12,12 @@ from openconcept.examples.N3_HybridSingleAisle_Refrig import run_hybrid_sa_analysis from openconcept.examples.minimal import setup_problem as setup_minimal_problem from openconcept.examples.minimal_integrator import MissionAnalysisWithFuelBurn as MinimalIntegratorMissionAnalysis + try: from openconcept.examples.B738_VLM_drag import run_738_analysis as run_738VLM_analysis from openconcept.aerodynamics.openaerostruct import VLMDataGen, OASDataGen from openconcept.examples.B738_aerostructural import run_738_analysis as run_738Aerostruct_analysis + OAS_installed = True except: OAS_installed = False @@ -29,10 +30,11 @@ def setUp(self): def test_values_TBM(self): prob = self.prob - assert_near_equal(prob.get_val('climb.OEW', units='lb'), 4756.772140709275, tolerance=1e-5) - assert_near_equal(prob.get_val('rotate.range_final', units='ft'), 2490.89174399, tolerance=1e-5) - assert_near_equal(prob.get_val('engineoutclimb.gamma',units='deg'), 8.78263, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lb'), 633.58800032, tolerance=1e-5) + assert_near_equal(prob.get_val("climb.OEW", units="lb"), 4756.772140709275, tolerance=1e-5) + assert_near_equal(prob.get_val("rotate.range_final", units="ft"), 2490.89174399, tolerance=1e-5) + assert_near_equal(prob.get_val("engineoutclimb.gamma", units="deg"), 8.78263, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lb"), 633.58800032, tolerance=1e-5) + class CaravanAnalysisTestCase(unittest.TestCase): def setUp(self): @@ -40,8 +42,9 @@ def setUp(self): def test_values_Caravan(self): prob = self.prob - assert_near_equal(prob.get_val('v1vr.range_final', units='ft'), 1375.59921952, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lb'), 379.90334044, tolerance=1e-5) + assert_near_equal(prob.get_val("v1vr.range_final", units="ft"), 1375.59921952, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lb"), 379.90334044, tolerance=1e-5) + class HybridTwinThermalTestCase(unittest.TestCase): def setUp(self): @@ -49,22 +52,29 @@ def setUp(self): def test_values_thermalhybridtwin(self): prob = self.prob - assert_near_equal(prob.get_val('climb.OEW', units='lb'), 6673.001027260613, tolerance=1e-5) - assert_near_equal(prob.get_val('rotate.range_final', units='ft'), 4434.68545427, tolerance=1e-5) - assert_near_equal(prob.get_val('engineoutclimb.gamma',units='deg'), 1.75074018, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lb'), 862.69811822, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.propmodel.batt1.SOC_final', units=None), -3.80158704e-05, tolerance=1e-5) - - assert_near_equal(prob.get_val('climb.propmodel.motorheatsink.T', units='degC')[-1], 76.19938727507775, tolerance=1e-5) - assert_near_equal(prob.get_val('climb.propmodel.batteryheatsink.T', units='degC')[-1], -0.27586540922391123, tolerance=1e-5) - assert_near_equal(prob.get_val('cruise.propmodel.duct.drag', units='lbf')[0], 7.968332825694923, tolerance=1e-5) + assert_near_equal(prob.get_val("climb.OEW", units="lb"), 6673.001027260613, tolerance=1e-5) + assert_near_equal(prob.get_val("rotate.range_final", units="ft"), 4434.68545427, tolerance=1e-5) + assert_near_equal(prob.get_val("engineoutclimb.gamma", units="deg"), 1.75074018, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lb"), 862.69811822, tolerance=1e-5) + assert_near_equal( + prob.get_val("descent.propmodel.batt1.SOC_final", units=None), -3.80158704e-05, tolerance=1e-5 + ) + + assert_near_equal( + prob.get_val("climb.propmodel.motorheatsink.T", units="degC")[-1], 76.19938727507775, tolerance=1e-5 + ) + assert_near_equal( + prob.get_val("climb.propmodel.batteryheatsink.T", units="degC")[-1], -0.27586540922391123, tolerance=1e-5 + ) + assert_near_equal(prob.get_val("cruise.propmodel.duct.drag", units="lbf")[0], 7.968332825694923, tolerance=1e-5) # changelog 10/2020 - updated most of the values due to minor update to hydraulic diam calculation in the heat exchanger + # 10/2021 commenting out because does not converge with the new chiller in chiller.py # class HybridTwinActiveThermalTestCase(unittest.TestCase): # def setUp(self): # self.prob = run_hybrid_twin_active_thermal_analysis() - + # def test_values_hybridtwin(self): # prob = self.prob @@ -91,59 +101,64 @@ def test_values_thermalhybridtwin(self): # assert_near_equal(prob.get_val('cruise.propmodel.duct.drag', units='lbf')[-1], 1.5888992670493287, tolerance=1e-5) # # changelog 10/2020 - updated most of the values due to minor update to hydraulic diam calculation in the heat exchanger + class HybridTwinTestCase(unittest.TestCase): def setUp(self): self.prob = run_hybrid_twin_analysis() - + def test_values_hybridtwin(self): prob = self.prob - assert_near_equal(prob.get_val('climb.OEW', units='lb'), 6648.424765080086, tolerance=1e-5) - assert_near_equal(prob.get_val('rotate.range_final', units='ft'), 4383.871458066499, tolerance=1e-5) - assert_near_equal(prob.get_val('engineoutclimb.gamma',units='deg'), 1.7659046316724112, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lb'), 854.8937776195904, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.propmodel.batt1.SOC_final', units=None), -0.00030626412, tolerance=1e-5) + assert_near_equal(prob.get_val("climb.OEW", units="lb"), 6648.424765080086, tolerance=1e-5) + assert_near_equal(prob.get_val("rotate.range_final", units="ft"), 4383.871458066499, tolerance=1e-5) + assert_near_equal(prob.get_val("engineoutclimb.gamma", units="deg"), 1.7659046316724112, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lb"), 854.8937776195904, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.propmodel.batt1.SOC_final", units=None), -0.00030626412, tolerance=1e-5) class KingAirTestCase(unittest.TestCase): def setUp(self): self.prob = run_kingair_analysis() - + def test_values_kingair(self): prob = self.prob - assert_near_equal(prob.get_val('climb.OEW', units='lb'), 6471.539115423346, tolerance=1e-5) - assert_near_equal(prob.get_val('rotate.range_final', units='ft'), 3054.61279799, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lb'), 1666.73459582, tolerance=1e-5) + assert_near_equal(prob.get_val("climb.OEW", units="lb"), 6471.539115423346, tolerance=1e-5) + assert_near_equal(prob.get_val("rotate.range_final", units="ft"), 3054.61279799, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lb"), 1666.73459582, tolerance=1e-5) class ElectricSingleTestCase(unittest.TestCase): def setUp(self): self.prob = run_electricsingle_analysis() - + def test_values_electricsingle(self): prob = self.prob - assert_near_equal(prob.get_val('rotate.range_final', units='ft'), 2419.111568458725, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.propmodel.batt1.SOC')[-1], 0.1663373102614198, tolerance=1e-5) - assert_near_equal(prob.get_val('descent.propmodel.motorheatsink.T', units='degC')[-1], 14.906950172494192, tolerance=1e-5) + assert_near_equal(prob.get_val("rotate.range_final", units="ft"), 2419.111568458725, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.propmodel.batt1.SOC")[-1], 0.1663373102614198, tolerance=1e-5) + assert_near_equal( + prob.get_val("descent.propmodel.motorheatsink.T", units="degC")[-1], 14.906950172494192, tolerance=1e-5 + ) # changelog 10/2020 - heat sink T now 14.90695 after minor change to hydraulic diameter computation in heat exchanger + class B738TestCase(unittest.TestCase): def setUp(self): self.prob = run_738_analysis() - + def test_values_B738(self): prob = self.prob # block fuel - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lbm'), 28549.432517, tolerance=3e-4) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lbm"), 28549.432517, tolerance=3e-4) # changelog: 9/2020 - previously 28688.329, updated CFM surrogate model to reject spurious high Mach, low altitude points # total fuel - assert_near_equal(prob.get_val('loiter.fuel_used_final', units='lbm'), 34424.68533072, tolerance=3e-4) + assert_near_equal(prob.get_val("loiter.fuel_used_final", units="lbm"), 34424.68533072, tolerance=3e-4) # changelog: 9/2020 - previously 34555.313, updated CFM surrogate model to reject spurious high Mach, low altitude points + @unittest.skipIf(not OAS_installed, "OpenAeroStruct is not installed") class B738VLMTestCase(unittest.TestCase): def setUp(self): self.prob = run_738VLM_analysis() - + def tearDown(self): # Get rid of any specified surface options in the VLMDataGen # class after every test. This is necessary because the class @@ -156,15 +171,16 @@ def tearDown(self): def test_values_B738(self): prob = self.prob # block fuel - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lbm'), 28443.39604559, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lbm"), 28443.39604559, tolerance=1e-5) # total fuel - assert_near_equal(prob.get_val('loiter.fuel_used_final', units='lbm'), 34075.30721371, tolerance=1e-5) + assert_near_equal(prob.get_val("loiter.fuel_used_final", units="lbm"), 34075.30721371, tolerance=1e-5) + @unittest.skipIf(not OAS_installed, "OpenAeroStruct is not installed") class B738AerostructTestCase(unittest.TestCase): def setUp(self): self.prob = run_738Aerostruct_analysis() - + def tearDown(self): # Get rid of any specified surface options in the OASDataGen # class after every test. This is necessary because the class @@ -173,31 +189,69 @@ def tearDown(self): # surface options. Doing this prevents that error when doing # multiple tests with different surface options. del OASDataGen.surf_options - + def test_values_B738(self): prob = self.prob # block fuel - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lbm'), 34310.44045734, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lbm"), 34310.44045734, tolerance=1e-5) + class N3HSATestCase(unittest.TestCase): def setUp(self): self.prob = run_hybrid_sa_analysis(plots=False) - + def test_values_N3HSA(self): prob = self.prob # block fuel (no reserve, since the N+3 HSA uses the basic 3-phase mission) - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lbm'), 9006.52397811, tolerance=1e-5) + assert_near_equal(prob.get_val("descent.fuel_used_final", units="lbm"), 9006.52397811, tolerance=1e-5) + class MinimalTestCase(unittest.TestCase): def setUp(self): self.prob = setup_minimal_problem() self.prob.run_model() - + def test_values_minimal(self): # No fuel burn, so check the throttle from the three phases - assert_near_equal(self.prob.get_val('mission.climb.throttle'), np.array([0.651459, 0.647949, 0.644480, 0.641052, 0.637664, 0.634317, 0.631010, 0.627744, 0.624519, 0.621333, 0.618189]), tolerance=1e-5) - assert_near_equal(self.prob.get_val('mission.cruise.throttle'), np.full(11, 0.490333), tolerance=1e-5) - assert_near_equal(self.prob.get_val('mission.descent.throttle'), np.array([0.362142, 0.358981, 0.355778, 0.352535, 0.349250, 0.345924, 0.342557, 0.339149, 0.335699, 0.332207, 0.328674]), tolerance=1e-5) + assert_near_equal( + self.prob.get_val("mission.climb.throttle"), + np.array( + [ + 0.651459, + 0.647949, + 0.644480, + 0.641052, + 0.637664, + 0.634317, + 0.631010, + 0.627744, + 0.624519, + 0.621333, + 0.618189, + ] + ), + tolerance=1e-5, + ) + assert_near_equal(self.prob.get_val("mission.cruise.throttle"), np.full(11, 0.490333), tolerance=1e-5) + assert_near_equal( + self.prob.get_val("mission.descent.throttle"), + np.array( + [ + 0.362142, + 0.358981, + 0.355778, + 0.352535, + 0.349250, + 0.345924, + 0.342557, + 0.339149, + 0.335699, + 0.332207, + 0.328674, + ] + ), + tolerance=1e-5, + ) class MinimalIntegratorTestCase(unittest.TestCase): @@ -206,8 +260,10 @@ def setUp(self): self.prob.run_model() def test_values_minimal(self): - assert_near_equal(self.prob.get_val('mission.descent.fuel_integrator.fuel_burned_final'), 633.350, tolerance=1e-5) + assert_near_equal( + self.prob.get_val("mission.descent.fuel_integrator.fuel_burned_final"), 633.350, tolerance=1e-5 + ) -if __name__=="__main__": +if __name__ == "__main__": unittest.main() diff --git a/openconcept/mission/mission_groups.py b/openconcept/mission/mission_groups.py index d7b43da5..8b3226cc 100644 --- a/openconcept/mission/mission_groups.py +++ b/openconcept/mission/mission_groups.py @@ -1,12 +1,12 @@ -import openmdao.api as om +import openmdao.api as om import numpy as np from openconcept.utilities import Integrator, AddSubtractComp, ElementMultiplyDivideComp -import warnings +import warnings # OpenConcept PhaseGroup will be used to hold analysis phases with time integration def find_integrators_in_model(system, abs_namespace, timevars, states): - durationvar = system._problem_meta['oc_time_var'] + durationvar = system._problem_meta["oc_time_var"] # check if we are a group or not if isinstance(system, om.Group): @@ -14,69 +14,89 @@ def find_integrators_in_model(system, abs_namespace, timevars, states): if not abs_namespace: next_namespace = subsys.name else: - next_namespace = abs_namespace + '.' + subsys.name + next_namespace = abs_namespace + "." + subsys.name find_integrators_in_model(subsys, next_namespace, timevars, states) else: # if the duration variable shows up we need to add its absolute path to timevars if isinstance(system, Integrator): - for varname in system._var_rel_names['input']: + for varname in system._var_rel_names["input"]: if varname == durationvar: - timevars.append(abs_namespace + '.' + varname) + timevars.append(abs_namespace + "." + varname) for state in system._state_vars.keys(): state_options = system._state_vars[state] - state_tuple = (abs_namespace + '.' + state_options['name'], - abs_namespace + '.' + state_options['start_name'], - abs_namespace + '.' + state_options['end_name']) + state_tuple = ( + abs_namespace + "." + state_options["name"], + abs_namespace + "." + state_options["start_name"], + abs_namespace + "." + state_options["end_name"], + ) states.append(state_tuple) + class PhaseGroup(om.Group): def __init__(self, **kwargs): # BB what if user isn't passing num_nodes to the phases? - num_nodes = kwargs.get('num_nodes', 1) + num_nodes = kwargs.get("num_nodes", 1) super(PhaseGroup, self).__init__(**kwargs) - self._oc_time_var_name = 'duration' + self._oc_time_var_name = "duration" self._oc_num_nodes = num_nodes def initialize(self): - self.options.declare('num_nodes', default=1, types=int, lower=0) + self.options.declare("num_nodes", default=1, types=int, lower=0) def _setup_procs(self, pathname, comm, mode, prob_meta): # need to pass down the name of the duration variable via prob_meta - prob_meta.update({'oc_time_var': self._oc_time_var_name}) - prob_meta.update({'oc_num_nodes': self._oc_num_nodes}) + prob_meta.update({"oc_time_var": self._oc_time_var_name}) + prob_meta.update({"oc_num_nodes": self._oc_num_nodes}) super(PhaseGroup, self)._setup_procs(pathname, comm, mode, prob_meta) - + def configure(self): # check child subsys for variables to be integrated and add them all timevars = [] states = [] - find_integrators_in_model(self, '', timevars, states) + find_integrators_in_model(self, "", timevars, states) self._setup_var_data() # make connections from duration to integrated vars automatically time_prom_addresses_already_connected = [] for var_abs_address in timevars: if self.pathname: - var_abs_address = self.pathname + '.' + var_abs_address - var_prom_address = self._var_abs2prom['input'][var_abs_address] - if var_prom_address != self._oc_time_var_name and var_prom_address not in time_prom_addresses_already_connected: + var_abs_address = self.pathname + "." + var_abs_address + var_prom_address = self._var_abs2prom["input"][var_abs_address] + if ( + var_prom_address != self._oc_time_var_name + and var_prom_address not in time_prom_addresses_already_connected + ): self.connect(self._oc_time_var_name, var_prom_address) time_prom_addresses_already_connected.append(var_prom_address) self._oc_states_list = states + class IntegratorGroup(om.Group): def __init__(self, **kwargs): # BB what if user isn't passing num_nodes to the phases? - time_units = kwargs.pop('time_units', 's') + time_units = kwargs.pop("time_units", "s") super(IntegratorGroup, self).__init__(**kwargs) self._oc_time_units = time_units self._n_auto_comps = 0 - - - def promote_add(self, sources, prom_name, promoted_sources=[], factors=None, vec_size=1, units=None, length=1, val=1.0, - res_units=None, desc='', lower=None, upper=None, ref=1.0, - ref0=0.0, res_ref=None): + def promote_add( + self, + sources, + prom_name, + promoted_sources=[], + factors=None, + vec_size=1, + units=None, + length=1, + val=1.0, + res_units=None, + desc="", + lower=None, + upper=None, + ref=1.0, + ref0=0.0, + res_ref=None, + ): """Helper function called during setup""" add_index = self._n_auto_comps self._n_auto_comps += 1 @@ -84,84 +104,106 @@ def promote_add(self, sources, prom_name, promoted_sources=[], factors=None, vec n_inputs = len(sources) + len(promoted_sources) if factors is None: factors = np.ones(n_inputs) - adder.add_equation(output_name=prom_name, - input_names=['_temp'+str(i) for i in range(n_inputs)], - scaling_factors=factors, - vec_size=vec_size, - units=units, - length=length, - val=val, - res_units=res_units, - desc=desc, - lower=lower, - upper=upper, - ref=ref, - ref0=ref0, - res_ref=res_ref) - adder_name = 'add'+str(add_index) + adder.add_equation( + output_name=prom_name, + input_names=["_temp" + str(i) for i in range(n_inputs)], + scaling_factors=factors, + vec_size=vec_size, + units=units, + length=length, + val=val, + res_units=res_units, + desc=desc, + lower=lower, + upper=upper, + ref=ref, + ref0=ref0, + res_ref=res_ref, + ) + adder_name = "add" + str(add_index) prom_in = [] for i, promoted_source in enumerate(promoted_sources): - prom_in.append(('_temp'+str(i+len(sources)), promoted_source)) + prom_in.append(("_temp" + str(i + len(sources)), promoted_source)) - self.add_subsystem(adder_name, adder, promotes_inputs=prom_in, promotes_outputs=['*']) + self.add_subsystem(adder_name, adder, promotes_inputs=prom_in, promotes_outputs=["*"]) for i, source in enumerate(sources): - self.connect(source, adder_name+'._temp'+str(i)) + self.connect(source, adder_name + "._temp" + str(i)) - def promote_mult(self, source, prom_name, factor, vec_size=1, units=None, length=1, val=1.0, - res_units=None, desc='', lower=None, upper=None, ref=1.0, - ref0=0.0, res_ref=None, - divide=None, input_units=None, tags=None): + def promote_mult( + self, + source, + prom_name, + factor, + vec_size=1, + units=None, + length=1, + val=1.0, + res_units=None, + desc="", + lower=None, + upper=None, + ref=1.0, + ref0=0.0, + res_ref=None, + divide=None, + input_units=None, + tags=None, + ): """Helper function called during setup""" mult_index = self._n_auto_comps self._n_auto_comps += 1 - mult = ElementMultiplyDivideComp(output_name=prom_name, - input_names=['_temp'], - scaling_factor=factor, - input_units=[units], - vec_size=vec_size, - length=length, - val=val, - res_units=res_units, - desc=desc, - lower=lower, - upper=upper, - ref=ref, - ref0=ref0, - res_ref=res_ref, - divide=divide, - tags=tags) - mult_name = 'mult'+str(mult_index) - self.add_subsystem(mult_name, mult, promotes_outputs=['*']) - self.connect(source, mult_name+'._temp') - + mult = ElementMultiplyDivideComp( + output_name=prom_name, + input_names=["_temp"], + scaling_factor=factor, + input_units=[units], + vec_size=vec_size, + length=length, + val=val, + res_units=res_units, + desc=desc, + lower=lower, + upper=upper, + ref=ref, + ref0=ref0, + res_ref=res_ref, + divide=divide, + tags=tags, + ) + mult_name = "mult" + str(mult_index) + self.add_subsystem(mult_name, mult, promotes_outputs=["*"]) + self.connect(source, mult_name + "._temp") def _setup_procs(self, pathname, comm, mode, prob_meta): time_units = self._oc_time_units self._under_dymos = False self._under_openconcept = False try: - num_nodes = prob_meta['oc_num_nodes'] + num_nodes = prob_meta["oc_num_nodes"] self._under_openconcept = True except KeyError: # TODO test_if_under_dymos if not self._under_dymos: - raise NameError('Integrator group must be created within an OpenConcept phase or Dymos trajectory') + raise NameError("Integrator group must be created within an OpenConcept phase or Dymos trajectory") if self._under_openconcept: - self.add_subsystem('ode_integ', Integrator(time_setup='duration', method='simpson',diff_units=time_units, num_nodes=num_nodes)) + self.add_subsystem( + "ode_integ", + Integrator(time_setup="duration", method="simpson", diff_units=time_units, num_nodes=num_nodes), + ) super(IntegratorGroup, self)._setup_procs(pathname, comm, mode, prob_meta) def configure(self): self._setup_var_data() - parent_meta_dict = self.get_io_metadata(iotypes='output', tags='integrate') + parent_meta_dict = self.get_io_metadata(iotypes="output", tags="integrate") for subsys, _ in self._subsystems_allprocs.values(): # exclude any group subclasses, they don't have real outputs if not isinstance(subsys, om.Group): # find any variables that have an 'integrate tag - child_meta_dict = subsys.get_io_metadata(iotypes='output', tags='integrate') + child_meta_dict = subsys.get_io_metadata(iotypes="output", tags="integrate") for var in child_meta_dict.keys(): - tags = child_meta_dict[var]['tags'] + tags = child_meta_dict[var]["tags"] state_name = None state_units = None state_val = 0.0 @@ -171,35 +213,51 @@ def configure(self): # TODO Check for duplicates otherwise generic Openmdao duplicate output/input error raised for tag in tags: - split_tag = tag.split(':') - if split_tag[0] == 'state_name': + split_tag = tag.split(":") + if split_tag[0] == "state_name": state_name = split_tag[-1] - elif split_tag[0] == 'state_units': + elif split_tag[0] == "state_units": state_units = split_tag[-1] - elif split_tag[0] == 'state_val': + elif split_tag[0] == "state_val": state_val = eval(split_tag[-1]) - elif split_tag[0] == 'state_lower': + elif split_tag[0] == "state_lower": state_lower = float(split_tag[-1]) - elif split_tag[0] == 'state_upper': + elif split_tag[0] == "state_upper": state_upper = float(split_tag[-1]) - elif split_tag[0] == 'state_promotes': + elif split_tag[0] == "state_promotes": state_promotes = eval(split_tag[-1]) if state_name is None: - raise ValueError('Must provide a state_name tag for integrated variable '+subsys.name+'.'+var) + raise ValueError( + "Must provide a state_name tag for integrated variable " + subsys.name + "." + var + ) if state_units is None: - warnings.warn('OpenConcept integration variable '+subsys.name+'.'+var+' '+'has no units specified. This can be dangerous.') - self.ode_integ.add_integrand(state_name, rate_name=var, val=state_val, - units=state_units, lower=state_lower, upper=state_upper) + warnings.warn( + "OpenConcept integration variable " + + subsys.name + + "." + + var + + " " + + "has no units specified. This can be dangerous." + ) + self.ode_integ.add_integrand( + state_name, + rate_name=var, + val=state_val, + units=state_units, + lower=state_lower, + upper=state_upper, + ) # make the rate connection - rate_var_abs_address = subsys.name+'.'+var + rate_var_abs_address = subsys.name + "." + var # if self.pathname: # rate_var_abs_address = self.pathname + '.' + rate_var_abs_address - rate_var_prom_address = parent_meta_dict[rate_var_abs_address]['prom_name'] - self.connect(rate_var_prom_address, 'ode_integ'+'.'+var) + rate_var_prom_address = parent_meta_dict[rate_var_abs_address]["prom_name"] + self.connect(rate_var_prom_address, "ode_integ" + "." + var) if state_promotes: - self.ode_integ._var_promotes['output'].append((state_name, None)) - self.ode_integ._var_promotes['output'].append((state_name+'_final',None)) - self.ode_integ._var_promotes['input'].append((state_name+'_initial',None)) + self.ode_integ._var_promotes["output"].append((state_name, None)) + self.ode_integ._var_promotes["output"].append((state_name + "_final", None)) + self.ode_integ._var_promotes["input"].append((state_name + "_initial", None)) + class TrajectoryGroup(om.Group): def __init__(self, **kwargs): @@ -210,7 +268,7 @@ def _configure(self): super(TrajectoryGroup, self)._configure() for linkage in self._oc_phases_to_link: self._link_phases(linkage[0], linkage[1], linkage[2]) - + def _link_phases(self, phase1, phase2, states_to_skip=[]): # find all the states in each phase # if they appear in both phase1 and phase2, connect them @@ -222,22 +280,22 @@ def _link_phases(self, phase1, phase2, states_to_skip=[]): self._setup_var_data() for state_tuple in phase1_states: if state_tuple[0] in [state_tuple_2[0] for state_tuple_2 in phase2_states]: - - phase1_abs_name = phase1.name + '.' + state_tuple[0] - phase1_end_abs_name = phase1.name + '.' + state_tuple[2] # final - phase2_start_abs_name = phase2.name + '.' + state_tuple[1] # initial + + phase1_abs_name = phase1.name + "." + state_tuple[0] + phase1_end_abs_name = phase1.name + "." + state_tuple[2] # final + phase2_start_abs_name = phase2.name + "." + state_tuple[1] # initial if self.pathname: - phase1_abs_name = self.pathname + '.' + phase1_abs_name - phase1_end_abs_name = self.pathname + '.' + phase1_end_abs_name - phase2_start_abs_name = self.pathname + '.' + phase2_start_abs_name - - phase1_prom_name = self._var_abs2prom['output'][phase1_abs_name] - if phase1_prom_name.startswith(phase1.name): # only modify the text if it starts with the prefix - state_prom_name = phase1_prom_name.replace(phase1.name+'.', "", 1) + phase1_abs_name = self.pathname + "." + phase1_abs_name + phase1_end_abs_name = self.pathname + "." + phase1_end_abs_name + phase2_start_abs_name = self.pathname + "." + phase2_start_abs_name + + phase1_prom_name = self._var_abs2prom["output"][phase1_abs_name] + if phase1_prom_name.startswith(phase1.name): # only modify the text if it starts with the prefix + state_prom_name = phase1_prom_name.replace(phase1.name + ".", "", 1) else: state_prom_name = phase1_prom_name - phase1_end_prom_name = self._var_abs2prom['output'][phase1_end_abs_name] - phase2_start_prom_name = self._var_abs2prom['input'][phase2_start_abs_name] + phase1_end_prom_name = self._var_abs2prom["output"][phase1_end_abs_name] + phase2_start_prom_name = self._var_abs2prom["input"][phase2_start_abs_name] if not (state_tuple[0] in states_to_skip): if not (state_prom_name in states_to_skip): self.connect(phase1_end_prom_name, phase2_start_prom_name) @@ -245,5 +303,5 @@ def _link_phases(self, phase1, phase2, states_to_skip=[]): def link_phases(self, phase1, phase2, states_to_skip=[]): # need to cache this because the data we need isn't ready yet if not isinstance(phase1, PhaseGroup) or not isinstance(phase2, PhaseGroup): - raise ValueError('link_phases phase arguments must be OpenConcept PhaseGroup objects') - self._oc_phases_to_link.append((phase1, phase2, states_to_skip)) \ No newline at end of file + raise ValueError("link_phases phase arguments must be OpenConcept PhaseGroup objects") + self._oc_phases_to_link.append((phase1, phase2, states_to_skip)) diff --git a/openconcept/mission/phases.py b/openconcept/mission/phases.py index a2728489..6455a5de 100644 --- a/openconcept/mission/phases.py +++ b/openconcept/mission/phases.py @@ -6,6 +6,7 @@ from openconcept.utilities.constants import GRAV_CONST import numpy as np + class ClimbAngleComp(ExplicitComponent): """ Computes steady climb angle based on excess thrust. @@ -32,27 +33,28 @@ class ClimbAngleComp(ExplicitComponent): num_nodes : int Number of points to run """ + def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_input('drag', units='N',shape=(nn,)) - self.add_input('weight', units='kg', shape=(nn,)) - self.add_input('thrust', units='N',shape=(nn,)) - self.add_output('gamma', units='rad',shape=(nn,)) + nn = self.options["num_nodes"] + self.add_input("drag", units="N", shape=(nn,)) + self.add_input("weight", units="kg", shape=(nn,)) + self.add_input("thrust", units="N", shape=(nn,)) + self.add_output("gamma", units="rad", shape=(nn,)) - self.declare_partials(['gamma'], ['weight','thrust','drag'], cols=np.arange(0,nn), rows=np.arange(0,nn)) + self.declare_partials(["gamma"], ["weight", "thrust", "drag"], cols=np.arange(0, nn), rows=np.arange(0, nn)) def compute(self, inputs, outputs): - outputs['gamma'] = np.arcsin((inputs['thrust']-inputs['drag'])/inputs['weight']/GRAV_CONST) + outputs["gamma"] = np.arcsin((inputs["thrust"] - inputs["drag"]) / inputs["weight"] / GRAV_CONST) def compute_partials(self, inputs, J): - interior_qty = (inputs['thrust']-inputs['drag'])/inputs['weight']/GRAV_CONST - d_arcsin = 1/np.sqrt(1-interior_qty**2) - J['gamma','thrust'] = d_arcsin/inputs['weight']/GRAV_CONST - J['gamma','drag'] = -d_arcsin/inputs['weight']/GRAV_CONST - J['gamma','weight'] = -d_arcsin*(inputs['thrust']-inputs['drag'])/inputs['weight']**2/GRAV_CONST + interior_qty = (inputs["thrust"] - inputs["drag"]) / inputs["weight"] / GRAV_CONST + d_arcsin = 1 / np.sqrt(1 - interior_qty**2) + J["gamma", "thrust"] = d_arcsin / inputs["weight"] / GRAV_CONST + J["gamma", "drag"] = -d_arcsin / inputs["weight"] / GRAV_CONST + J["gamma", "weight"] = -d_arcsin * (inputs["thrust"] - inputs["drag"]) / inputs["weight"] ** 2 / GRAV_CONST class FlipVectorComp(ExplicitComponent): @@ -83,30 +85,37 @@ class FlipVectorComp(ExplicitComponent): Units for vec_in and vec_out (Default None) Specify as an OpenMDAO unit string (e.g. 'kg') """ + def initialize(self): - self.options.declare('num_nodes',default=1) - self.options.declare('negative',default=False) - self.options.declare('units',default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("negative", default=False) + self.options.declare("units", default=None) def setup(self): - nn = self.options['num_nodes'] - units = self.options['units'] - self.add_input('vec_in', units=units, shape=(nn,)) - self.add_output('vec_out', units=units, shape=(nn,)) - negative = self.options['negative'] + nn = self.options["num_nodes"] + units = self.options["units"] + self.add_input("vec_in", units=units, shape=(nn,)) + self.add_output("vec_out", units=units, shape=(nn,)) + negative = self.options["negative"] if negative: scaler = -1 else: scaler = 1 - self.declare_partials(['vec_out'],['vec_in'],rows=np.arange(nn-1,-1,-1),cols=np.arange(0,nn,1),val=scaler*np.ones((nn,))) + self.declare_partials( + ["vec_out"], + ["vec_in"], + rows=np.arange(nn - 1, -1, -1), + cols=np.arange(0, nn, 1), + val=scaler * np.ones((nn,)), + ) def compute(self, inputs, outputs): - negative = self.options['negative'] + negative = self.options["negative"] if negative: scaler = -1 else: scaler = 1 - outputs['vec_out'] = scaler * np.flip(inputs['vec_in'], 0) + outputs["vec_out"] = scaler * np.flip(inputs["vec_in"], 0) class BFLImplicitSolve(ImplicitComponent): @@ -135,47 +144,56 @@ class BFLImplicitSolve(ImplicitComponent): Decision speed (scalar, m/s) """ + def setup(self): - self.add_input('distance_continue', units='m') - self.add_input('distance_abort', units='m') - self.add_input('takeoff|vr', units='m/s') - self.add_output('takeoff|v1', units='m/s',val=20,lower=10,upper=150) - self.declare_partials('takeoff|v1',['distance_continue','distance_abort','takeoff|v1','takeoff|vr']) + self.add_input("distance_continue", units="m") + self.add_input("distance_abort", units="m") + self.add_input("takeoff|vr", units="m/s") + self.add_output("takeoff|v1", units="m/s", val=20, lower=10, upper=150) + self.declare_partials("takeoff|v1", ["distance_continue", "distance_abort", "takeoff|v1", "takeoff|vr"]) def apply_nonlinear(self, inputs, outputs, residuals): speedtol = 1e-1 disttol = 0 - #force the decision speed to zero - if inputs['takeoff|vr'] < outputs['takeoff|v1'] + speedtol: - residuals['takeoff|v1'] = inputs['takeoff|vr'] - outputs['takeoff|v1'] + # force the decision speed to zero + if inputs["takeoff|vr"] < outputs["takeoff|v1"] + speedtol: + residuals["takeoff|v1"] = inputs["takeoff|vr"] - outputs["takeoff|v1"] else: - residuals['takeoff|v1'] = inputs['distance_continue'] - inputs['distance_abort'] - - #if you are within vtol on the correct side but the stopping distance bigger, use the regular mode - if inputs['takeoff|vr'] >= outputs['takeoff|v1'] and inputs['takeoff|vr'] - outputs['takeoff|v1'] < speedtol and (inputs['distance_abort'] - inputs['distance_continue']) > disttol: - residuals['takeoff|v1'] = inputs['distance_continue'] - inputs['distance_abort'] + residuals["takeoff|v1"] = inputs["distance_continue"] - inputs["distance_abort"] + # if you are within vtol on the correct side but the stopping distance bigger, use the regular mode + if ( + inputs["takeoff|vr"] >= outputs["takeoff|v1"] + and inputs["takeoff|vr"] - outputs["takeoff|v1"] < speedtol + and (inputs["distance_abort"] - inputs["distance_continue"]) > disttol + ): + residuals["takeoff|v1"] = inputs["distance_continue"] - inputs["distance_abort"] def linearize(self, inputs, outputs, partials): speedtol = 1e-1 disttol = 0 - if inputs['takeoff|vr'] < outputs['takeoff|v1'] + speedtol: - partials['takeoff|v1','distance_continue'] = 0 - partials['takeoff|v1','distance_abort'] = 0 - partials['takeoff|v1','takeoff|vr'] = 1 - partials['takeoff|v1','takeoff|v1'] = -1 + if inputs["takeoff|vr"] < outputs["takeoff|v1"] + speedtol: + partials["takeoff|v1", "distance_continue"] = 0 + partials["takeoff|v1", "distance_abort"] = 0 + partials["takeoff|v1", "takeoff|vr"] = 1 + partials["takeoff|v1", "takeoff|v1"] = -1 else: - partials['takeoff|v1','distance_continue'] = 1 - partials['takeoff|v1','distance_abort'] = -1 - partials['takeoff|v1','takeoff|vr'] = 0 - partials['takeoff|v1','takeoff|v1'] = 0 + partials["takeoff|v1", "distance_continue"] = 1 + partials["takeoff|v1", "distance_abort"] = -1 + partials["takeoff|v1", "takeoff|vr"] = 0 + partials["takeoff|v1", "takeoff|v1"] = 0 + + if ( + inputs["takeoff|vr"] >= outputs["takeoff|v1"] + and inputs["takeoff|vr"] - outputs["takeoff|v1"] < speedtol + and (inputs["distance_abort"] - inputs["distance_continue"]) > disttol + ): + partials["takeoff|v1", "distance_continue"] = 1 + partials["takeoff|v1", "distance_abort"] = -1 + partials["takeoff|v1", "takeoff|vr"] = 0 + partials["takeoff|v1", "takeoff|v1"] = 0 - if inputs['takeoff|vr'] >= outputs['takeoff|v1'] and inputs['takeoff|vr'] - outputs['takeoff|v1'] < speedtol and (inputs['distance_abort'] - inputs['distance_continue']) > disttol: - partials['takeoff|v1','distance_continue'] = 1 - partials['takeoff|v1','distance_abort'] = -1 - partials['takeoff|v1','takeoff|vr'] = 0 - partials['takeoff|v1','takeoff|v1'] = 0 class Groundspeeds(ExplicitComponent): """ @@ -205,42 +223,61 @@ class Groundspeeds(ExplicitComponent): num_nodes : int Number of points to run """ + def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of Simpson intervals to use per seg (eg. climb, cruise, descend). Number of analysis points is 2N+1") + self.options.declare( + "num_nodes", + default=1, + desc="Number of Simpson intervals to use per seg (eg. climb, cruise, descend). Number of analysis points is 2N+1", + ) def setup(self): - nn = self.options['num_nodes'] - self.add_input('fltcond|vs', units='m/s',shape=(nn,)) - self.add_input('fltcond|Utrue', units='m/s',shape=(nn,)) - self.add_output('fltcond|groundspeed', units='m/s',shape=(nn,)) - self.add_output('fltcond|cosgamma', shape=(nn,), desc='Cosine of the flight path angle') - self.add_output('fltcond|singamma', shape=(nn,), desc='sin of the flight path angle' ) - self.declare_partials(['fltcond|groundspeed','fltcond|cosgamma','fltcond|singamma'], ['fltcond|vs','fltcond|Utrue'], rows=range(nn), cols=range(nn)) + nn = self.options["num_nodes"] + self.add_input("fltcond|vs", units="m/s", shape=(nn,)) + self.add_input("fltcond|Utrue", units="m/s", shape=(nn,)) + self.add_output("fltcond|groundspeed", units="m/s", shape=(nn,)) + self.add_output("fltcond|cosgamma", shape=(nn,), desc="Cosine of the flight path angle") + self.add_output("fltcond|singamma", shape=(nn,), desc="sin of the flight path angle") + self.declare_partials( + ["fltcond|groundspeed", "fltcond|cosgamma", "fltcond|singamma"], + ["fltcond|vs", "fltcond|Utrue"], + rows=range(nn), + cols=range(nn), + ) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - #compute the groundspeed on climb and desc - inside = inputs['fltcond|Utrue']**2-inputs['fltcond|vs']**2 - groundspeed = np.sqrt(inside) + nn = self.options["num_nodes"] + # compute the groundspeed on climb and desc + inside = inputs["fltcond|Utrue"] ** 2 - inputs["fltcond|vs"] ** 2 + groundspeed = np.sqrt(inside) groundspeed_fixed = np.sqrt(np.where(np.less(inside, 0.0), 0.01, inside)) - #groundspeed = np.sqrt(inputs['fltcond|Utrue']**2-inputs['fltcond|vs']**2) - #groundspeed_fixed= np.where(np.isnan(groundspeed),0,groundspeed) - outputs['fltcond|groundspeed'] = groundspeed_fixed - outputs['fltcond|singamma'] = np.where(np.isnan(groundspeed),1,inputs['fltcond|vs'] / inputs['fltcond|Utrue']) - outputs['fltcond|cosgamma'] = groundspeed_fixed / inputs['fltcond|Utrue'] + # groundspeed = np.sqrt(inputs['fltcond|Utrue']**2-inputs['fltcond|vs']**2) + # groundspeed_fixed= np.where(np.isnan(groundspeed),0,groundspeed) + outputs["fltcond|groundspeed"] = groundspeed_fixed + outputs["fltcond|singamma"] = np.where(np.isnan(groundspeed), 1, inputs["fltcond|vs"] / inputs["fltcond|Utrue"]) + outputs["fltcond|cosgamma"] = groundspeed_fixed / inputs["fltcond|Utrue"] def compute_partials(self, inputs, J): - inside = inputs['fltcond|Utrue']**2-inputs['fltcond|vs']**2 - groundspeed = np.sqrt(inside) + inside = inputs["fltcond|Utrue"] ** 2 - inputs["fltcond|vs"] ** 2 + groundspeed = np.sqrt(inside) groundspeed_fixed = np.sqrt(np.where(np.less(inside, 0.0), 0.01, inside)) - J['fltcond|groundspeed','fltcond|vs'] = np.where(np.isnan(groundspeed),0,(1/2) / groundspeed_fixed * (-2) * inputs['fltcond|vs']) - J['fltcond|groundspeed','fltcond|Utrue'] = np.where(np.isnan(groundspeed),0, (1/2) / groundspeed_fixed * 2 * inputs['fltcond|Utrue']) - J['fltcond|singamma','fltcond|vs'] = np.where(np.isnan(groundspeed), 0, 1 / inputs['fltcond|Utrue']) - J['fltcond|singamma','fltcond|Utrue'] = np.where(np.isnan(groundspeed), 0, - inputs['fltcond|vs'] / inputs['fltcond|Utrue'] ** 2) - J['fltcond|cosgamma','fltcond|vs'] = J['fltcond|groundspeed','fltcond|vs'] / inputs['fltcond|Utrue'] - J['fltcond|cosgamma','fltcond|Utrue'] = (J['fltcond|groundspeed','fltcond|Utrue'] * inputs['fltcond|Utrue'] - groundspeed_fixed) / inputs['fltcond|Utrue']**2 + J["fltcond|groundspeed", "fltcond|vs"] = np.where( + np.isnan(groundspeed), 0, (1 / 2) / groundspeed_fixed * (-2) * inputs["fltcond|vs"] + ) + J["fltcond|groundspeed", "fltcond|Utrue"] = np.where( + np.isnan(groundspeed), 0, (1 / 2) / groundspeed_fixed * 2 * inputs["fltcond|Utrue"] + ) + J["fltcond|singamma", "fltcond|vs"] = np.where(np.isnan(groundspeed), 0, 1 / inputs["fltcond|Utrue"]) + J["fltcond|singamma", "fltcond|Utrue"] = np.where( + np.isnan(groundspeed), 0, -inputs["fltcond|vs"] / inputs["fltcond|Utrue"] ** 2 + ) + J["fltcond|cosgamma", "fltcond|vs"] = J["fltcond|groundspeed", "fltcond|vs"] / inputs["fltcond|Utrue"] + J["fltcond|cosgamma", "fltcond|Utrue"] = ( + J["fltcond|groundspeed", "fltcond|Utrue"] * inputs["fltcond|Utrue"] - groundspeed_fixed + ) / inputs["fltcond|Utrue"] ** 2 + class HorizontalAcceleration(ExplicitComponent): """ @@ -271,39 +308,51 @@ class HorizontalAcceleration(ExplicitComponent): num_nodes : int Number of analysis points to run """ + def initialize(self): - self.options.declare('num_nodes',default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_input('weight', units='kg', shape=(nn,)) - self.add_input('drag', units='N',shape=(nn,)) - self.add_input('lift', units='N',shape=(nn,)) - self.add_input('thrust', units='N',shape=(nn,)) - self.add_input('fltcond|singamma',shape=(nn,)) - self.add_input('braking',shape=(nn,)) - - self.add_output('accel_horiz', units='m/s**2', shape=(nn,)) - arange=np.arange(nn) - self.declare_partials(['accel_horiz'], ['weight','drag','lift','thrust','braking'], rows=arange, cols=arange) - self.declare_partials(['accel_horiz'], ['fltcond|singamma'], rows=arange, cols=arange, val=-GRAV_CONST*np.ones((nn,))) - + nn = self.options["num_nodes"] + self.add_input("weight", units="kg", shape=(nn,)) + self.add_input("drag", units="N", shape=(nn,)) + self.add_input("lift", units="N", shape=(nn,)) + self.add_input("thrust", units="N", shape=(nn,)) + self.add_input("fltcond|singamma", shape=(nn,)) + self.add_input("braking", shape=(nn,)) + + self.add_output("accel_horiz", units="m/s**2", shape=(nn,)) + arange = np.arange(nn) + self.declare_partials( + ["accel_horiz"], ["weight", "drag", "lift", "thrust", "braking"], rows=arange, cols=arange + ) + self.declare_partials( + ["accel_horiz"], ["fltcond|singamma"], rows=arange, cols=arange, val=-GRAV_CONST * np.ones((nn,)) + ) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - m = inputs['weight'] - floor_vec = np.where(np.less((GRAV_CONST-inputs['lift']/m),0.0),0.0,1.0) - accel = inputs['thrust']/m - inputs['drag']/m - floor_vec*inputs['braking']*(GRAV_CONST-inputs['lift']/m) - GRAV_CONST*inputs['fltcond|singamma'] - outputs['accel_horiz'] = accel + nn = self.options["num_nodes"] + m = inputs["weight"] + floor_vec = np.where(np.less((GRAV_CONST - inputs["lift"] / m), 0.0), 0.0, 1.0) + accel = ( + inputs["thrust"] / m + - inputs["drag"] / m + - floor_vec * inputs["braking"] * (GRAV_CONST - inputs["lift"] / m) + - GRAV_CONST * inputs["fltcond|singamma"] + ) + outputs["accel_horiz"] = accel def compute_partials(self, inputs, J): - m = inputs['weight'] - floor_vec = np.where(np.less((GRAV_CONST-inputs['lift']/m),0.0),0.0,1.0) - J['accel_horiz','thrust'] = 1/m - J['accel_horiz','drag'] = -1/m - J['accel_horiz','braking'] = -floor_vec*(GRAV_CONST-inputs['lift']/m) - J['accel_horiz','lift'] = floor_vec*inputs['braking']/m - J['accel_horiz','weight'] = (inputs['drag']-inputs['thrust']-floor_vec*inputs['braking']*inputs['lift'])/m**2 + m = inputs["weight"] + floor_vec = np.where(np.less((GRAV_CONST - inputs["lift"] / m), 0.0), 0.0, 1.0) + J["accel_horiz", "thrust"] = 1 / m + J["accel_horiz", "drag"] = -1 / m + J["accel_horiz", "braking"] = -floor_vec * (GRAV_CONST - inputs["lift"] / m) + J["accel_horiz", "lift"] = floor_vec * inputs["braking"] / m + J["accel_horiz", "weight"] = ( + inputs["drag"] - inputs["thrust"] - floor_vec * inputs["braking"] * inputs["lift"] + ) / m**2 + class VerticalAcceleration(ExplicitComponent): """ @@ -335,42 +384,50 @@ class VerticalAcceleration(ExplicitComponent): num_nodes : int Number of analysis points to run """ + def initialize(self): - self.options.declare('num_nodes',default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_input('weight', units='kg', shape=(nn,)) - self.add_input('drag', units='N',shape=(nn,)) - self.add_input('lift', units='N',shape=(nn,)) - self.add_input('thrust', units='N',shape=(nn,)) - self.add_input('fltcond|singamma',shape=(nn,)) - self.add_input('fltcond|cosgamma',shape=(nn,)) - - self.add_output('accel_vert', units='m/s**2', shape=(nn,),upper=2.5*GRAV_CONST,lower=-1*GRAV_CONST) - arange=np.arange(nn) - self.declare_partials(['accel_vert'], ['weight','drag','lift','thrust','fltcond|singamma','fltcond|cosgamma'], rows=arange, cols=arange) - + nn = self.options["num_nodes"] + self.add_input("weight", units="kg", shape=(nn,)) + self.add_input("drag", units="N", shape=(nn,)) + self.add_input("lift", units="N", shape=(nn,)) + self.add_input("thrust", units="N", shape=(nn,)) + self.add_input("fltcond|singamma", shape=(nn,)) + self.add_input("fltcond|cosgamma", shape=(nn,)) + + self.add_output("accel_vert", units="m/s**2", shape=(nn,), upper=2.5 * GRAV_CONST, lower=-1 * GRAV_CONST) + arange = np.arange(nn) + self.declare_partials( + ["accel_vert"], + ["weight", "drag", "lift", "thrust", "fltcond|singamma", "fltcond|cosgamma"], + rows=arange, + cols=arange, + ) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - cosg = inputs['fltcond|cosgamma'] - sing = inputs['fltcond|singamma'] - accel = (inputs['lift']*cosg + (inputs['thrust']-inputs['drag'])*sing - GRAV_CONST*inputs['weight'])/inputs['weight'] - accel = np.clip(accel, -GRAV_CONST, 2.5*GRAV_CONST) - outputs['accel_vert'] = accel + nn = self.options["num_nodes"] + cosg = inputs["fltcond|cosgamma"] + sing = inputs["fltcond|singamma"] + accel = ( + inputs["lift"] * cosg + (inputs["thrust"] - inputs["drag"]) * sing - GRAV_CONST * inputs["weight"] + ) / inputs["weight"] + accel = np.clip(accel, -GRAV_CONST, 2.5 * GRAV_CONST) + outputs["accel_vert"] = accel def compute_partials(self, inputs, J): - m = inputs['weight'] - cosg = inputs['fltcond|cosgamma'] - sing = inputs['fltcond|singamma'] + m = inputs["weight"] + cosg = inputs["fltcond|cosgamma"] + sing = inputs["fltcond|singamma"] + + J["accel_vert", "thrust"] = sing / m + J["accel_vert", "drag"] = -sing / m + J["accel_vert", "lift"] = cosg / m + J["accel_vert", "fltcond|singamma"] = (inputs["thrust"] - inputs["drag"]) / m + J["accel_vert", "fltcond|cosgamma"] = inputs["lift"] / m + J["accel_vert", "weight"] = -(inputs["lift"] * cosg + (inputs["thrust"] - inputs["drag"]) * sing) / m**2 - J['accel_vert','thrust'] = sing / m - J['accel_vert','drag'] = -sing / m - J['accel_vert','lift'] = cosg / m - J['accel_vert','fltcond|singamma'] = (inputs['thrust']-inputs['drag']) / m - J['accel_vert','fltcond|cosgamma'] = inputs['lift'] / m - J['accel_vert','weight'] = -(inputs['lift']*cosg + (inputs['thrust']-inputs['drag'])*sing)/m**2 class SteadyFlightCL(ExplicitComponent): """ @@ -400,29 +457,57 @@ class SteadyFlightCL(ExplicitComponent): num_nodes : int Number of analysis nodes to run """ + def initialize(self): - self.options.declare('num_nodes',default=5,desc="Number of Simpson intervals to use per seg (eg. climb, cruise, descend). Number of analysis points is 2N+1") + self.options.declare( + "num_nodes", + default=5, + desc="Number of Simpson intervals to use per seg (eg. climb, cruise, descend). Number of analysis points is 2N+1", + ) def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] arange = np.arange(nn) - self.add_input('weight', units='kg', shape=(nn,)) - self.add_input('fltcond|q', units='N * m**-2', shape=(nn,)) - self.add_input('ac|geom|wing|S_ref', units='m **2') - self.add_input('fltcond|cosgamma', val=1.0, shape=(nn,)) - self.add_output('fltcond|CL',shape=(nn,)) - self.declare_partials(['fltcond|CL'], ['weight','fltcond|q',"fltcond|cosgamma"], rows=arange, cols=arange) - self.declare_partials(['fltcond|CL'], ['ac|geom|wing|S_ref'], rows=arange, cols=np.zeros(nn)) + self.add_input("weight", units="kg", shape=(nn,)) + self.add_input("fltcond|q", units="N * m**-2", shape=(nn,)) + self.add_input("ac|geom|wing|S_ref", units="m **2") + self.add_input("fltcond|cosgamma", val=1.0, shape=(nn,)) + self.add_output("fltcond|CL", shape=(nn,)) + self.declare_partials(["fltcond|CL"], ["weight", "fltcond|q", "fltcond|cosgamma"], rows=arange, cols=arange) + self.declare_partials(["fltcond|CL"], ["ac|geom|wing|S_ref"], rows=arange, cols=np.zeros(nn)) def compute(self, inputs, outputs): - outputs['fltcond|CL'] = inputs['fltcond|cosgamma']*GRAV_CONST*inputs['weight']/inputs['fltcond|q']/inputs['ac|geom|wing|S_ref'] + outputs["fltcond|CL"] = ( + inputs["fltcond|cosgamma"] + * GRAV_CONST + * inputs["weight"] + / inputs["fltcond|q"] + / inputs["ac|geom|wing|S_ref"] + ) def compute_partials(self, inputs, J): - J['fltcond|CL','weight'] = inputs['fltcond|cosgamma']*GRAV_CONST/inputs['fltcond|q']/inputs['ac|geom|wing|S_ref'] - J['fltcond|CL','fltcond|q'] = - inputs['fltcond|cosgamma']*GRAV_CONST*inputs['weight'] / inputs['fltcond|q']**2 / inputs['ac|geom|wing|S_ref'] - J['fltcond|CL','ac|geom|wing|S_ref'] = - inputs['fltcond|cosgamma']*GRAV_CONST*inputs['weight'] / inputs['fltcond|q'] / inputs['ac|geom|wing|S_ref']**2 - J['fltcond|CL','fltcond|cosgamma'] = GRAV_CONST*inputs['weight']/inputs['fltcond|q']/inputs['ac|geom|wing|S_ref'] + J["fltcond|CL", "weight"] = ( + inputs["fltcond|cosgamma"] * GRAV_CONST / inputs["fltcond|q"] / inputs["ac|geom|wing|S_ref"] + ) + J["fltcond|CL", "fltcond|q"] = ( + -inputs["fltcond|cosgamma"] + * GRAV_CONST + * inputs["weight"] + / inputs["fltcond|q"] ** 2 + / inputs["ac|geom|wing|S_ref"] + ) + J["fltcond|CL", "ac|geom|wing|S_ref"] = ( + -inputs["fltcond|cosgamma"] + * GRAV_CONST + * inputs["weight"] + / inputs["fltcond|q"] + / inputs["ac|geom|wing|S_ref"] ** 2 + ) + J["fltcond|CL", "fltcond|cosgamma"] = ( + GRAV_CONST * inputs["weight"] / inputs["fltcond|q"] / inputs["ac|geom|wing|S_ref"] + ) + class GroundRollPhase(PhaseGroup): """ @@ -488,80 +573,174 @@ class GroundRollPhase(PhaseGroup): """ def initialize(self): - self.options.declare('num_nodes',default=1) - self.options.declare('flight_phase',default=None,desc='Phase of flight e.g. v0v1, cruise') - self.options.declare('aircraft_model',default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None, desc="Phase of flight e.g. v0v1, cruise") + self.options.declare("aircraft_model", default=None) def setup(self): - nn = self.options['num_nodes'] - ivcomp = self.add_subsystem('const_settings', IndepVarComp(), promotes_outputs=["*"]) + nn = self.options["num_nodes"] + ivcomp = self.add_subsystem("const_settings", IndepVarComp(), promotes_outputs=["*"]) # set CL = 0.1 for the ground roll per Raymer's book - ivcomp.add_output('fltcond|CL', val=np.ones((nn,))*0.1) - ivcomp.add_output('vr_vstall_mult',val=1.1) - ivcomp.add_output('fltcond|h',val=np.zeros((nn,)),units='m') - ivcomp.add_output('fltcond|vs',val=np.zeros((nn,)),units='m/s') - ivcomp.add_output('zero_speed',val=2,units='m/s') - - - flight_phase = self.options['flight_phase'] - if flight_phase == 'v0v1': - ivcomp.add_output('braking',val=np.ones((nn,))*0.03) - ivcomp.add_output('propulsor_active',val=np.ones((nn,))) - ivcomp.add_output('throttle',val=np.ones((nn,))) + ivcomp.add_output("fltcond|CL", val=np.ones((nn,)) * 0.1) + ivcomp.add_output("vr_vstall_mult", val=1.1) + ivcomp.add_output("fltcond|h", val=np.zeros((nn,)), units="m") + ivcomp.add_output("fltcond|vs", val=np.zeros((nn,)), units="m/s") + ivcomp.add_output("zero_speed", val=2, units="m/s") + + flight_phase = self.options["flight_phase"] + if flight_phase == "v0v1": + ivcomp.add_output("braking", val=np.ones((nn,)) * 0.03) + ivcomp.add_output("propulsor_active", val=np.ones((nn,))) + ivcomp.add_output("throttle", val=np.ones((nn,))) zero_start = True - elif flight_phase == 'v1vr': - ivcomp.add_output('braking',val=np.ones((nn,))*0.03) - ivcomp.add_output('propulsor_active',val=np.zeros((nn,))) - ivcomp.add_output('throttle',val=np.ones((nn,))) + elif flight_phase == "v1vr": + ivcomp.add_output("braking", val=np.ones((nn,)) * 0.03) + ivcomp.add_output("propulsor_active", val=np.zeros((nn,))) + ivcomp.add_output("throttle", val=np.ones((nn,))) zero_start = False - elif flight_phase == 'v1v0': - ivcomp.add_output('braking',val=0.4*np.ones((nn,))) - ivcomp.add_output('propulsor_active',val=np.zeros((nn,))) - ivcomp.add_output('throttle',val=np.zeros((nn,))) - zero_start=False + elif flight_phase == "v1v0": + ivcomp.add_output("braking", val=0.4 * np.ones((nn,))) + ivcomp.add_output("propulsor_active", val=np.zeros((nn,))) + ivcomp.add_output("throttle", val=np.zeros((nn,))) + zero_start = False - self.add_subsystem('atmos', ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=True), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('gs',Groundspeeds(num_nodes=nn),promotes_inputs=['*'],promotes_outputs=['*']) + self.add_subsystem( + "atmos", + ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=True), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem("gs", Groundspeeds(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) # add the user-defined aircraft model - self.add_subsystem('acmodel',self.options['aircraft_model'](num_nodes=nn,flight_phase=self.options['flight_phase']),promotes_inputs=['*'],promotes_outputs=['*']) - - self.add_subsystem('lift',Lift(num_nodes=nn), promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('stall',StallSpeed(),promotes_inputs=[('CLmax','ac|aero|CLmax_TO'),('weight','ac|weights|MTOW'),'ac|geom|wing|S_ref'],promotes_outputs=['*']) - self.add_subsystem('vrspeed',ElementMultiplyDivideComp(output_name='takeoff|vr',input_names=['Vstall_eas','vr_vstall_mult'],input_units=['m/s',None]),promotes_inputs=['*'],promotes_outputs=['*']) - - - self.add_subsystem('haccel',HorizontalAcceleration(num_nodes=nn), promotes_inputs=['*'],promotes_outputs=['*']) - if flight_phase == 'v1v0': - #unfortunately need to shoot backwards to avoid negative airspeeds - #reverse the order of the accelerations so the last one is first (and make them negative) - self.add_subsystem('flipaccel', FlipVectorComp(num_nodes=nn, units='m/s**2', negative=True), promotes_inputs=[('vec_in','accel_horiz')]) - #integrate the timesteps in reverse from near zero speed. - ode_integ = self.add_subsystem('ode_integ_phase', Integrator(num_nodes=nn, method='simpson', diff_units='s',time_setup='duration'), promotes_inputs=['*'], promotes_outputs=['*']) - ode_integ.add_integrand('vel_q', units='m/s', rate_name='vel_dqdt', start_name='zero_speed', end_name='fltcond|Utrue_initial', lower=1.5) - self.connect('flipaccel.vec_out','vel_dqdt') - #flip the result of the reverse integration again so the flight condition is forward and consistent with everythign else - self.add_subsystem('flipvel', FlipVectorComp(num_nodes=nn, units='m/s', negative=False), promotes_outputs=[('vec_out','fltcond|Utrue')]) - self.connect('vel_q','flipvel.vec_in') + self.add_subsystem( + "acmodel", + self.options["aircraft_model"](num_nodes=nn, flight_phase=self.options["flight_phase"]), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + + self.add_subsystem("lift", Lift(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "stall", + StallSpeed(), + promotes_inputs=[("CLmax", "ac|aero|CLmax_TO"), ("weight", "ac|weights|MTOW"), "ac|geom|wing|S_ref"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "vrspeed", + ElementMultiplyDivideComp( + output_name="takeoff|vr", input_names=["Vstall_eas", "vr_vstall_mult"], input_units=["m/s", None] + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + + self.add_subsystem( + "haccel", HorizontalAcceleration(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + if flight_phase == "v1v0": + # unfortunately need to shoot backwards to avoid negative airspeeds + # reverse the order of the accelerations so the last one is first (and make them negative) + self.add_subsystem( + "flipaccel", + FlipVectorComp(num_nodes=nn, units="m/s**2", negative=True), + promotes_inputs=[("vec_in", "accel_horiz")], + ) + # integrate the timesteps in reverse from near zero speed. + ode_integ = self.add_subsystem( + "ode_integ_phase", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + ode_integ.add_integrand( + "vel_q", + units="m/s", + rate_name="vel_dqdt", + start_name="zero_speed", + end_name="fltcond|Utrue_initial", + lower=1.5, + ) + self.connect("flipaccel.vec_out", "vel_dqdt") + # flip the result of the reverse integration again so the flight condition is forward and consistent with everythign else + self.add_subsystem( + "flipvel", + FlipVectorComp(num_nodes=nn, units="m/s", negative=False), + promotes_outputs=[("vec_out", "fltcond|Utrue")], + ) + self.connect("vel_q", "flipvel.vec_in") # now set the time step so that backwards shooting results in the correct 'initial' segment airspeed - self.add_subsystem('v0constraint',BalanceComp(name='duration',units='s',eq_units='m/s',rhs_name='fltcond|Utrue_initial',lhs_name='takeoff|v1',val=10.,upper=100.,lower=1.), - promotes_inputs=['*'],promotes_outputs=['duration']) + self.add_subsystem( + "v0constraint", + BalanceComp( + name="duration", + units="s", + eq_units="m/s", + rhs_name="fltcond|Utrue_initial", + lhs_name="takeoff|v1", + val=10.0, + upper=100.0, + lower=1.0, + ), + promotes_inputs=["*"], + promotes_outputs=["duration"], + ) else: # forward shooting for these acceleration phases - ode_integ = self.add_subsystem('ode_integ_phase', Integrator(num_nodes=nn, method='simpson', diff_units='s',time_setup='duration'), promotes_inputs=['*'], promotes_outputs=['*']) - ode_integ.add_integrand('fltcond|Utrue', units='m/s', rate_name='accel_horiz', start_name='fltcond|Utrue_initial', end_name='fltcond|Utrue_final', lower=1.5) - if flight_phase == 'v0v1': - self.connect('zero_speed','fltcond|Utrue_initial') - self.add_subsystem('v1constraint',BalanceComp(name='duration',units='s',eq_units='m/s',rhs_name='fltcond|Utrue_final',lhs_name='takeoff|v1',val=10.,upper=100.,lower=1.), - promotes_inputs=['*'],promotes_outputs=['duration']) - elif flight_phase == 'v1vr': - self.add_subsystem('vrconstraint',BalanceComp(name='duration',units='s',eq_units='m/s',rhs_name='fltcond|Utrue_final',lhs_name='takeoff|vr',val=5.,upper=12.,lower=0.0), - promotes_inputs=['*'],promotes_outputs=['duration']) + ode_integ = self.add_subsystem( + "ode_integ_phase", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + ode_integ.add_integrand( + "fltcond|Utrue", + units="m/s", + rate_name="accel_horiz", + start_name="fltcond|Utrue_initial", + end_name="fltcond|Utrue_final", + lower=1.5, + ) + if flight_phase == "v0v1": + self.connect("zero_speed", "fltcond|Utrue_initial") + self.add_subsystem( + "v1constraint", + BalanceComp( + name="duration", + units="s", + eq_units="m/s", + rhs_name="fltcond|Utrue_final", + lhs_name="takeoff|v1", + val=10.0, + upper=100.0, + lower=1.0, + ), + promotes_inputs=["*"], + promotes_outputs=["duration"], + ) + elif flight_phase == "v1vr": + self.add_subsystem( + "vrconstraint", + BalanceComp( + name="duration", + units="s", + eq_units="m/s", + rhs_name="fltcond|Utrue_final", + lhs_name="takeoff|vr", + val=5.0, + upper=12.0, + lower=0.0, + ), + promotes_inputs=["*"], + promotes_outputs=["duration"], + ) if zero_start: - ode_integ.add_integrand('range', rate_name='fltcond|groundspeed', units='m', zero_start=True) + ode_integ.add_integrand("range", rate_name="fltcond|groundspeed", units="m", zero_start=True) else: - ode_integ.add_integrand('range', rate_name='fltcond|groundspeed', units='m') + ode_integ.add_integrand("range", rate_name="fltcond|groundspeed", units="m") + class RotationPhase(PhaseGroup): """ @@ -630,44 +809,94 @@ class RotationPhase(PhaseGroup): """ def initialize(self): - self.options.declare('num_nodes',default=1) - self.options.declare('flight_phase',default=None) - self.options.declare('aircraft_model',default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None) + self.options.declare("aircraft_model", default=None) def setup(self): - nn = self.options['num_nodes'] - ivcomp = self.add_subsystem('const_settings', IndepVarComp(), promotes_outputs=["*"]) - ivcomp.add_output('CL_rotate_mult', val=np.ones((nn,))*0.83) - ivcomp.add_output('h_obs', val=35, units='ft') - flight_phase = self.options['flight_phase'] - if flight_phase == 'rotate': - ivcomp.add_output('braking',val=np.zeros((nn,))) - ivcomp.add_output('propulsor_active',val=np.zeros((nn,))) - ivcomp.add_output('throttle',val=np.ones((nn,))) - - self.add_subsystem('atmos', ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=True), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('gs',Groundspeeds(num_nodes=nn),promotes_inputs=['*'],promotes_outputs=['*']) - clcomp = self.add_subsystem('clcomp',ElementMultiplyDivideComp(output_name='fltcond|CL', input_names=['CL_rotate_mult','ac|aero|CLmax_TO'], - vec_size=[nn,1], length=1), - promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('acmodel',self.options['aircraft_model'](num_nodes=nn,flight_phase=self.options['flight_phase']),promotes_inputs=['*'],promotes_outputs=['*']) - - - self.add_subsystem('lift',Lift(num_nodes=nn), promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('haccel',HorizontalAcceleration(num_nodes=nn), promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('vaccel',VerticalAcceleration(num_nodes=nn), promotes_inputs=['*'],promotes_outputs=['*']) - + nn = self.options["num_nodes"] + ivcomp = self.add_subsystem("const_settings", IndepVarComp(), promotes_outputs=["*"]) + ivcomp.add_output("CL_rotate_mult", val=np.ones((nn,)) * 0.83) + ivcomp.add_output("h_obs", val=35, units="ft") + flight_phase = self.options["flight_phase"] + if flight_phase == "rotate": + ivcomp.add_output("braking", val=np.zeros((nn,))) + ivcomp.add_output("propulsor_active", val=np.zeros((nn,))) + ivcomp.add_output("throttle", val=np.ones((nn,))) + + self.add_subsystem( + "atmos", + ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=True), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem("gs", Groundspeeds(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + clcomp = self.add_subsystem( + "clcomp", + ElementMultiplyDivideComp( + output_name="fltcond|CL", input_names=["CL_rotate_mult", "ac|aero|CLmax_TO"], vec_size=[nn, 1], length=1 + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "acmodel", + self.options["aircraft_model"](num_nodes=nn, flight_phase=self.options["flight_phase"]), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + + self.add_subsystem("lift", Lift(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "haccel", HorizontalAcceleration(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("vaccel", VerticalAcceleration(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + # TODO always starts from zero altitude - self.add_subsystem('clear_obstacle',BalanceComp(name='duration',units='s',val=1,eq_units='m',rhs_name='fltcond|h_final',lhs_name='h_obs',lower=0.1,upper=15), - promotes_inputs=['*'],promotes_outputs=['duration']) - int1 = self.add_subsystem('intvelocity', Integrator(num_nodes=nn, method='simpson',diff_units='s',time_setup='duration'), promotes_outputs=['*'], promotes_inputs=['*']) - int1.add_integrand('fltcond|Utrue', rate_name='accel_horiz', units='m/s', lower=0.1) - int2 = self.add_subsystem('intrange', Integrator(num_nodes=nn, method='simpson',diff_units='s',time_setup='duration'), promotes_outputs=['*'], promotes_inputs=['*']) - int2.add_integrand('range', rate_name='fltcond|groundspeed', units='m') - int3 = self.add_subsystem('intvs', Integrator(num_nodes=nn, method='simpson',diff_units='s',time_setup='duration'), promotes_outputs=['*'], promotes_inputs=['*']) - int3.add_integrand('fltcond|vs', rate_name='accel_vert', units='m/s', zero_start=True) - int4 = self.add_subsystem('inth', Integrator(num_nodes=nn, method='simpson',diff_units='s',time_setup='duration'), promotes_outputs=['*'], promotes_inputs=['*']) - int4.add_integrand('fltcond|h', rate_name='fltcond|vs', units='m', zero_start=True) + self.add_subsystem( + "clear_obstacle", + BalanceComp( + name="duration", + units="s", + val=1, + eq_units="m", + rhs_name="fltcond|h_final", + lhs_name="h_obs", + lower=0.1, + upper=15, + ), + promotes_inputs=["*"], + promotes_outputs=["duration"], + ) + int1 = self.add_subsystem( + "intvelocity", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + int1.add_integrand("fltcond|Utrue", rate_name="accel_horiz", units="m/s", lower=0.1) + int2 = self.add_subsystem( + "intrange", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + int2.add_integrand("range", rate_name="fltcond|groundspeed", units="m") + int3 = self.add_subsystem( + "intvs", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + int3.add_integrand("fltcond|vs", rate_name="accel_vert", units="m/s", zero_start=True) + int4 = self.add_subsystem( + "inth", + Integrator(num_nodes=nn, method="simpson", diff_units="s", time_setup="duration"), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + int4.add_integrand("fltcond|h", rate_name="fltcond|vs", units="m", zero_start=True) + class SteadyFlightPhase(PhaseGroup): """ @@ -732,37 +961,71 @@ class SteadyFlightPhase(PhaseGroup): ac|weights|MTOW Maximum takeoff weight (scalar, kg) """ + def initialize(self): - self.options.declare('num_nodes',default=1) - self.options.declare('flight_phase',default=None,desc='Phase of flight e.g. v0v1, cruise') - self.options.declare('aircraft_model',default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None, desc="Phase of flight e.g. v0v1, cruise") + self.options.declare("aircraft_model", default=None) def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] # propulsor_active only exists in some aircraft models, so set_input_defaults # can't be used since it throws an error when it can't find an input - ivcomp = self.add_subsystem('const_settings', IndepVarComp(), promotes_outputs=["*"]) - ivcomp.add_output('propulsor_active', val=np.ones(nn)) - + ivcomp = self.add_subsystem("const_settings", IndepVarComp(), promotes_outputs=["*"]) + ivcomp.add_output("propulsor_active", val=np.ones(nn)) + # Use set_input_defaults as opposed to independent variable component to enable # users to connect linear interpolators to these inputs for "trajectory optimization" - self.set_input_defaults('braking', np.zeros(nn)) - self.set_input_defaults('fltcond|Ueas', np.ones((nn,))*90, units='m/s') - self.set_input_defaults('fltcond|vs', np.ones((nn,))*1, units='m/s') - self.set_input_defaults('zero_accel', np.zeros((nn,)), units='m/s**2') - - integ = self.add_subsystem('ode_integ_phase', Integrator(num_nodes=nn, diff_units='s', time_setup='duration', method='simpson'), promotes_inputs=['fltcond|vs', 'fltcond|groundspeed'], promotes_outputs=['fltcond|h', 'range']) - integ.add_integrand('fltcond|h', rate_name='fltcond|vs', val=1.0, units='m') - self.add_subsystem('atmos', ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=False), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('gs',Groundspeeds(num_nodes=nn),promotes_inputs=['*'],promotes_outputs=['*']) + self.set_input_defaults("braking", np.zeros(nn)) + self.set_input_defaults("fltcond|Ueas", np.ones((nn,)) * 90, units="m/s") + self.set_input_defaults("fltcond|vs", np.ones((nn,)) * 1, units="m/s") + self.set_input_defaults("zero_accel", np.zeros((nn,)), units="m/s**2") + + integ = self.add_subsystem( + "ode_integ_phase", + Integrator(num_nodes=nn, diff_units="s", time_setup="duration", method="simpson"), + promotes_inputs=["fltcond|vs", "fltcond|groundspeed"], + promotes_outputs=["fltcond|h", "range"], + ) + integ.add_integrand("fltcond|h", rate_name="fltcond|vs", val=1.0, units="m") + self.add_subsystem( + "atmos", + ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=False), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem("gs", Groundspeeds(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) # add the user-defined aircraft model - self.add_subsystem('acmodel',self.options['aircraft_model'](num_nodes=nn, flight_phase=self.options['flight_phase']),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('clcomp',SteadyFlightCL(num_nodes=nn), promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('lift',Lift(num_nodes=nn), promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('haccel',HorizontalAcceleration(num_nodes=nn), promotes_inputs=['*'],promotes_outputs=['*']) - integ.add_integrand('range', rate_name='fltcond|groundspeed', val=1.0, units='m') - self.add_subsystem('steadyflt',BalanceComp(name='throttle',val=np.ones((nn,))*0.5,lower=0.01,upper=1.05,units=None,normalize=False,eq_units='m/s**2',rhs_name='accel_horiz',lhs_name='zero_accel',rhs_val=np.zeros((nn,))), - promotes_inputs=['accel_horiz','zero_accel'],promotes_outputs=['throttle']) + self.add_subsystem( + "acmodel", + self.options["aircraft_model"](num_nodes=nn, flight_phase=self.options["flight_phase"]), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem("clcomp", SteadyFlightCL(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("lift", Lift(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "haccel", HorizontalAcceleration(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + integ.add_integrand("range", rate_name="fltcond|groundspeed", val=1.0, units="m") + self.add_subsystem( + "steadyflt", + BalanceComp( + name="throttle", + val=np.ones((nn,)) * 0.5, + lower=0.01, + upper=1.05, + units=None, + normalize=False, + eq_units="m/s**2", + rhs_name="accel_horiz", + lhs_name="zero_accel", + rhs_val=np.zeros((nn,)), + ), + promotes_inputs=["accel_horiz", "zero_accel"], + promotes_outputs=["throttle"], + ) + class ClimbAnglePhase(Group): """ @@ -822,32 +1085,65 @@ class ClimbAnglePhase(Group): """ def initialize(self): - self.options.declare('num_nodes',default=1) - self.options.declare('flight_phase',default=None,desc='Phase of flight e.g. v0v1, cruise') - self.options.declare('aircraft_model',default=None) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None, desc="Phase of flight e.g. v0v1, cruise") + self.options.declare("aircraft_model", default=None) def setup(self): - nn = self.options['num_nodes'] - ivcomp = self.add_subsystem('const_settings', IndepVarComp(), promotes_outputs=["*"]) - ivcomp.add_output('v2_vstall_mult',val=1.2) - ivcomp.add_output('fltcond|h',val=np.zeros((nn,)),units='m') - ivcomp.add_output('fltcond|cosgamma', val=np.ones((nn,))) - - flight_phase = self.options['flight_phase'] - if flight_phase == 'AllEngineClimbAngle': - ivcomp.add_output('propulsor_active',val=np.ones((nn,))) - ivcomp.add_output('throttle',val=np.ones((nn,))) - elif flight_phase == 'EngineOutClimbAngle': - ivcomp.add_output('propulsor_active',val=np.zeros((nn,))) - ivcomp.add_output('throttle',val=np.ones((nn,))) - self.add_subsystem('stall',StallSpeed(),promotes_inputs=[('CLmax','ac|aero|CLmax_TO'),('weight','ac|weights|MTOW'),'ac|geom|wing|S_ref'],promotes_outputs=['*']) - self.add_subsystem('vrspeed',ElementMultiplyDivideComp(output_name='takeoff|v2',input_names=['Vstall_eas','v2_vstall_mult'],input_units=['m/s',None]),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('atmos', ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=False), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('clcomp',SteadyFlightCL(num_nodes=nn), promotes_inputs=[('weight','ac|weights|MTOW'),'fltcond|*','ac|*'],promotes_outputs=['*']) - self.connect('takeoff|v2','fltcond|Ueas') + nn = self.options["num_nodes"] + ivcomp = self.add_subsystem("const_settings", IndepVarComp(), promotes_outputs=["*"]) + ivcomp.add_output("v2_vstall_mult", val=1.2) + ivcomp.add_output("fltcond|h", val=np.zeros((nn,)), units="m") + ivcomp.add_output("fltcond|cosgamma", val=np.ones((nn,))) + + flight_phase = self.options["flight_phase"] + if flight_phase == "AllEngineClimbAngle": + ivcomp.add_output("propulsor_active", val=np.ones((nn,))) + ivcomp.add_output("throttle", val=np.ones((nn,))) + elif flight_phase == "EngineOutClimbAngle": + ivcomp.add_output("propulsor_active", val=np.zeros((nn,))) + ivcomp.add_output("throttle", val=np.ones((nn,))) + self.add_subsystem( + "stall", + StallSpeed(), + promotes_inputs=[("CLmax", "ac|aero|CLmax_TO"), ("weight", "ac|weights|MTOW"), "ac|geom|wing|S_ref"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "vrspeed", + ElementMultiplyDivideComp( + output_name="takeoff|v2", input_names=["Vstall_eas", "v2_vstall_mult"], input_units=["m/s", None] + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "atmos", + ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=False), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "clcomp", + SteadyFlightCL(num_nodes=nn), + promotes_inputs=[("weight", "ac|weights|MTOW"), "fltcond|*", "ac|*"], + promotes_outputs=["*"], + ) + self.connect("takeoff|v2", "fltcond|Ueas") # the aircraft model needs to provide thrust and drag - self.add_subsystem('acmodel',self.options['aircraft_model'](num_nodes=nn,flight_phase=self.options['flight_phase']),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('climbangle',ClimbAngleComp(num_nodes=nn),promotes_inputs=['drag',('weight','ac|weights|MTOW'),'thrust'],promotes_outputs=['gamma']) + self.add_subsystem( + "acmodel", + self.options["aircraft_model"](num_nodes=nn, flight_phase=self.options["flight_phase"]), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "climbangle", + ClimbAngleComp(num_nodes=nn), + promotes_inputs=["drag", ("weight", "ac|weights|MTOW"), "thrust"], + promotes_outputs=["gamma"], + ) + class TakeoffTransition(ExplicitComponent): """ @@ -882,60 +1178,62 @@ class TakeoffTransition(ExplicitComponent): """ def initialize(self): - self.options.declare('h_obstacle',default=10.66,desc='Obstacle clearance height in m') - self.options.declare('load_factor', default=1.2, desc='Load factor during circular arc transition') + self.options.declare("h_obstacle", default=10.66, desc="Obstacle clearance height in m") + self.options.declare("load_factor", default=1.2, desc="Load factor during circular arc transition") + def setup(self): - self.add_input('fltcond|Utrue', units='m/s') - self.add_input('gamma', units='rad') - self.add_output('s_transition', units='m') - self.add_output('h_transition', units='m') - self.add_output('t_transition',units='s') - self.declare_partials(['s_transition','h_transition','t_transition'], ['fltcond|Utrue','gamma']) + self.add_input("fltcond|Utrue", units="m/s") + self.add_input("gamma", units="rad") + self.add_output("s_transition", units="m") + self.add_output("h_transition", units="m") + self.add_output("t_transition", units="s") + self.declare_partials(["s_transition", "h_transition", "t_transition"], ["fltcond|Utrue", "gamma"]) def compute(self, inputs, outputs): - hobs = self.options['h_obstacle'] - nfactor = self.options['load_factor'] - 1 - gam = inputs['gamma'] - ut = inputs['fltcond|Utrue'] - - R = ut**2/nfactor/GRAV_CONST - st = R*np.sin(gam) - ht = R*(1-np.cos(gam)) - #alternate formula if the obstacle is cleared during transition + hobs = self.options["h_obstacle"] + nfactor = self.options["load_factor"] - 1 + gam = inputs["gamma"] + ut = inputs["fltcond|Utrue"] + + R = ut**2 / nfactor / GRAV_CONST + st = R * np.sin(gam) + ht = R * (1 - np.cos(gam)) + # alternate formula if the obstacle is cleared during transition if ht > hobs: - st = np.sqrt(R**2-(R-hobs)**2) + st = np.sqrt(R**2 - (R - hobs) ** 2) ht = hobs - outputs['s_transition'] = st - outputs['h_transition'] = ht - outputs['t_transition'] = st / ut + outputs["s_transition"] = st + outputs["h_transition"] = ht + outputs["t_transition"] = st / ut def compute_partials(self, inputs, J): - hobs = self.options['h_obstacle'] - nfactor = self.options['load_factor'] - 1 - gam = inputs['gamma'] - ut = inputs['fltcond|Utrue'] - R = ut**2/nfactor/GRAV_CONST - dRdut = 2*ut/nfactor/GRAV_CONST - st = R*np.sin(gam) - ht = R*(1-np.cos(gam)) - #alternate formula if the obstacle is cleared during transition + hobs = self.options["h_obstacle"] + nfactor = self.options["load_factor"] - 1 + gam = inputs["gamma"] + ut = inputs["fltcond|Utrue"] + R = ut**2 / nfactor / GRAV_CONST + dRdut = 2 * ut / nfactor / GRAV_CONST + st = R * np.sin(gam) + ht = R * (1 - np.cos(gam)) + # alternate formula if the obstacle is cleared during transition if ht > hobs: - st = np.sqrt(R**2-(R-hobs)**2) - dstdut = 1/2/np.sqrt(R**2-(R-hobs)**2) * (2*R*dRdut - 2*(R-hobs)*dRdut) + st = np.sqrt(R**2 - (R - hobs) ** 2) + dstdut = 1 / 2 / np.sqrt(R**2 - (R - hobs) ** 2) * (2 * R * dRdut - 2 * (R - hobs) * dRdut) dstdgam = 0 dhtdut = 0 dhtdgam = 0 else: - dhtdut = dRdut*(1-np.cos(gam)) - dhtdgam = R*np.sin(gam) - dstdut = dRdut*np.sin(gam) - dstdgam = R*np.cos(gam) - J['s_transition','gamma'] = dstdgam - J['s_transition','fltcond|Utrue'] = dstdut - J['h_transition','gamma'] = dhtdgam - J['h_transition','fltcond|Utrue'] = dhtdut - J['t_transition','gamma'] = dstdgam / ut - J['t_transition','fltcond|Utrue'] = (dstdut * ut - st) / ut ** 2 + dhtdut = dRdut * (1 - np.cos(gam)) + dhtdgam = R * np.sin(gam) + dstdut = dRdut * np.sin(gam) + dstdgam = R * np.cos(gam) + J["s_transition", "gamma"] = dstdgam + J["s_transition", "fltcond|Utrue"] = dstdut + J["h_transition", "gamma"] = dhtdgam + J["h_transition", "fltcond|Utrue"] = dhtdut + J["t_transition", "gamma"] = dstdgam / ut + J["t_transition", "fltcond|Utrue"] = (dstdut * ut - st) / ut**2 + class TakeoffClimb(ExplicitComponent): """ @@ -962,37 +1260,38 @@ class TakeoffClimb(ExplicitComponent): """ def initialize(self): - self.options.declare('h_obstacle',default=10.66,desc='Obstacle clearance height in m') + self.options.declare("h_obstacle", default=10.66, desc="Obstacle clearance height in m") + def setup(self): - self.add_input('h_transition', units='m') - self.add_input('gamma', units='rad') - self.add_input('fltcond|Utrue', units='m/s') + self.add_input("h_transition", units="m") + self.add_input("gamma", units="rad") + self.add_input("fltcond|Utrue", units="m/s") - self.add_output('s_climb', units='m') - self.add_output('t_climb', units='s') - self.declare_partials(['s_climb'], ['h_transition','gamma']) - self.declare_partials(['t_climb'], ['h_transition','gamma','fltcond|Utrue']) + self.add_output("s_climb", units="m") + self.add_output("t_climb", units="s") + self.declare_partials(["s_climb"], ["h_transition", "gamma"]) + self.declare_partials(["t_climb"], ["h_transition", "gamma", "fltcond|Utrue"]) def compute(self, inputs, outputs): - hobs = self.options['h_obstacle'] - gam = inputs['gamma'] - ht = inputs['h_transition'] - ut = inputs['fltcond|Utrue'] - sc = (hobs-ht)/np.tan(gam) - outputs['s_climb'] = sc - outputs['t_climb'] = sc / ut + hobs = self.options["h_obstacle"] + gam = inputs["gamma"] + ht = inputs["h_transition"] + ut = inputs["fltcond|Utrue"] + sc = (hobs - ht) / np.tan(gam) + outputs["s_climb"] = sc + outputs["t_climb"] = sc / ut def compute_partials(self, inputs, J): - hobs = self.options['h_obstacle'] - gam = inputs['gamma'] - ht = inputs['h_transition'] - ut = inputs['fltcond|Utrue'] - sc = (hobs-ht)/np.tan(gam) - J['s_climb','gamma'] = -(hobs-ht)/np.tan(gam)**2 * (1/np.cos(gam))**2 - J['s_climb','h_transition'] = -1/np.tan(gam) - J['t_climb','gamma'] = J['s_climb','gamma'] / ut - J['t_climb','h_transition'] = J['s_climb','h_transition'] / ut - J['t_climb','fltcond|Utrue'] = - sc / ut ** 2 + hobs = self.options["h_obstacle"] + gam = inputs["gamma"] + ht = inputs["h_transition"] + ut = inputs["fltcond|Utrue"] + sc = (hobs - ht) / np.tan(gam) + J["s_climb", "gamma"] = -(hobs - ht) / np.tan(gam) ** 2 * (1 / np.cos(gam)) ** 2 + J["s_climb", "h_transition"] = -1 / np.tan(gam) + J["t_climb", "gamma"] = J["s_climb", "gamma"] / ut + J["t_climb", "h_transition"] = J["s_climb", "h_transition"] / ut + J["t_climb", "fltcond|Utrue"] = -sc / ut**2 class RobustRotationPhase(PhaseGroup): @@ -1050,54 +1349,127 @@ class RobustRotationPhase(PhaseGroup): """ def initialize(self): - self.options.declare('num_nodes',default=1) - self.options.declare('flight_phase',default=None,desc='Phase of flight e.g. v0v1, cruise') - self.options.declare('aircraft_model',default=None) - self.options.declare('h_obstacle',default=10.66, ) + self.options.declare("num_nodes", default=1) + self.options.declare("flight_phase", default=None, desc="Phase of flight e.g. v0v1, cruise") + self.options.declare("aircraft_model", default=None) + self.options.declare( + "h_obstacle", + default=10.66, + ) def setup(self): - nn = self.options['num_nodes'] - ivcomp = self.add_subsystem('const_settings', IndepVarComp(), promotes_outputs=["*"]) - flight_phase = self.options['flight_phase'] - if flight_phase == 'rotate': - ivcomp.add_output('braking',val=np.zeros((nn,))) - ivcomp.add_output('propulsor_active',val=np.zeros((nn,))) - ivcomp.add_output('throttle',val=np.ones((nn,))) + nn = self.options["num_nodes"] + ivcomp = self.add_subsystem("const_settings", IndepVarComp(), promotes_outputs=["*"]) + flight_phase = self.options["flight_phase"] + if flight_phase == "rotate": + ivcomp.add_output("braking", val=np.zeros((nn,))) + ivcomp.add_output("propulsor_active", val=np.zeros((nn,))) + ivcomp.add_output("throttle", val=np.ones((nn,))) # flight conditions are sea level takeoff, transition speed # split off a single node to compute climb angle # compute the transition distance and add it to range_initial # compute the transition time as a function of the groundspeed # provide transition time as duration - ivcomp.add_output('v2_vstall_mult',val=1.2) - ivcomp.add_output('vr_vstall_mult',val=1.1) - ivcomp.add_output('fltcond|vs', val=np.zeros((nn,)),units='m/s') - ivcomp.add_output('fltcond|cosgamma', val=np.ones((nn,)),units=None) - - - - ivcomp.add_output('h_obstacle',val=35,units='ft') - - self.add_subsystem('altitudes',LinearInterpolator(num_nodes=nn, units='m'),promotes_inputs=[('start_val','h_initial')],promotes_outputs=[('vec','fltcond|h')]) - self.connect('h_obstacle','altitudes.end_val') - - self.add_subsystem('stall',StallSpeed(),promotes_inputs=[('CLmax','ac|aero|CLmax_TO'),('weight','ac|weights|MTOW'),'ac|geom|wing|S_ref'],promotes_outputs=['*']) - self.add_subsystem('vrspeed',ElementMultiplyDivideComp(output_name='takeoff|vr',input_names=['Vstall_eas','vr_vstall_mult'],input_units=['m/s',None]),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('v2speed',ElementMultiplyDivideComp(output_name='takeoff|v2',input_names=['Vstall_eas','v2_vstall_mult'],input_units=['m/s',None]),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('speeds',LinearInterpolator(num_nodes=nn,units='kn'),promotes_inputs=[('start_val','takeoff|vr'),('end_val','takeoff|v2')],promotes_outputs=[('vec','fltcond|Ueas')]) - self.add_subsystem('atmos', ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=False), promotes_inputs=['*'], promotes_outputs=['*']) + ivcomp.add_output("v2_vstall_mult", val=1.2) + ivcomp.add_output("vr_vstall_mult", val=1.1) + ivcomp.add_output("fltcond|vs", val=np.zeros((nn,)), units="m/s") + ivcomp.add_output("fltcond|cosgamma", val=np.ones((nn,)), units=None) + + ivcomp.add_output("h_obstacle", val=35, units="ft") + + self.add_subsystem( + "altitudes", + LinearInterpolator(num_nodes=nn, units="m"), + promotes_inputs=[("start_val", "h_initial")], + promotes_outputs=[("vec", "fltcond|h")], + ) + self.connect("h_obstacle", "altitudes.end_val") + + self.add_subsystem( + "stall", + StallSpeed(), + promotes_inputs=[("CLmax", "ac|aero|CLmax_TO"), ("weight", "ac|weights|MTOW"), "ac|geom|wing|S_ref"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "vrspeed", + ElementMultiplyDivideComp( + output_name="takeoff|vr", input_names=["Vstall_eas", "vr_vstall_mult"], input_units=["m/s", None] + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "v2speed", + ElementMultiplyDivideComp( + output_name="takeoff|v2", input_names=["Vstall_eas", "v2_vstall_mult"], input_units=["m/s", None] + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "speeds", + LinearInterpolator(num_nodes=nn, units="kn"), + promotes_inputs=[("start_val", "takeoff|vr"), ("end_val", "takeoff|v2")], + promotes_outputs=[("vec", "fltcond|Ueas")], + ) + self.add_subsystem( + "atmos", + ComputeAtmosphericProperties(num_nodes=nn, true_airspeed_in=False), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) # pretty confident there's a simpler closed form multiple for CL at v2 - self.add_subsystem('clcomp',SteadyFlightCL(num_nodes=nn), promotes_inputs=['weight','fltcond|*','ac|*'],promotes_outputs=['*']) + self.add_subsystem( + "clcomp", + SteadyFlightCL(num_nodes=nn), + promotes_inputs=["weight", "fltcond|*", "ac|*"], + promotes_outputs=["*"], + ) # the aircraft model needs to provide thrust and drag - self.add_subsystem('acmodel',self.options['aircraft_model'](num_nodes=nn,flight_phase=self.options['flight_phase']),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('climbangle',ClimbAngleComp(num_nodes=nn),promotes_inputs=['drag','weight','thrust'],promotes_outputs=['gamma']) - self.add_subsystem('transition',TakeoffTransition(),promotes_outputs=['h_transition','s_transition','t_transition']) - self.promotes('transition', inputs=['fltcond|Utrue','gamma'], src_indices=[0], flat_src_indices=True) - self.add_subsystem('v2climb',TakeoffClimb(),promotes_inputs=['h_transition'],promotes_outputs=['s_climb','t_climb']) - self.promotes('v2climb', inputs=['fltcond|Utrue','gamma'], src_indices=[-1], flat_src_indices=True) - self.add_subsystem('tod_final',AddSubtractComp(output_name='range_final',input_names=['range_initial','s_transition','s_climb'],units='m'),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('duration',AddSubtractComp(output_name='duration',input_names=['t_transition','t_climb'],units='s'),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('h_final',AddSubtractComp(output_name='fltcond|h_final',input_names=['h_obstacle'],units='m'),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('ranges',LinearInterpolator(num_nodes=nn,units='m'),promotes_inputs=[('start_val','range_initial'),('end_val','range_final')],promotes_outputs=[('vec','range')]) - - - + self.add_subsystem( + "acmodel", + self.options["aircraft_model"](num_nodes=nn, flight_phase=self.options["flight_phase"]), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "climbangle", + ClimbAngleComp(num_nodes=nn), + promotes_inputs=["drag", "weight", "thrust"], + promotes_outputs=["gamma"], + ) + self.add_subsystem( + "transition", TakeoffTransition(), promotes_outputs=["h_transition", "s_transition", "t_transition"] + ) + self.promotes("transition", inputs=["fltcond|Utrue", "gamma"], src_indices=[0], flat_src_indices=True) + self.add_subsystem( + "v2climb", TakeoffClimb(), promotes_inputs=["h_transition"], promotes_outputs=["s_climb", "t_climb"] + ) + self.promotes("v2climb", inputs=["fltcond|Utrue", "gamma"], src_indices=[-1], flat_src_indices=True) + self.add_subsystem( + "tod_final", + AddSubtractComp( + output_name="range_final", input_names=["range_initial", "s_transition", "s_climb"], units="m" + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "duration", + AddSubtractComp(output_name="duration", input_names=["t_transition", "t_climb"], units="s"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "h_final", + AddSubtractComp(output_name="fltcond|h_final", input_names=["h_obstacle"], units="m"), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "ranges", + LinearInterpolator(num_nodes=nn, units="m"), + promotes_inputs=[("start_val", "range_initial"), ("end_val", "range_final")], + promotes_outputs=[("vec", "range")], + ) diff --git a/openconcept/mission/profiles.py b/openconcept/mission/profiles.py index e6fdee98..9a1fd5ea 100644 --- a/openconcept/mission/profiles.py +++ b/openconcept/mission/profiles.py @@ -1,8 +1,16 @@ -import openmdao.api as om +import openmdao.api as om from openconcept.utilities import DVLabel -from .phases import BFLImplicitSolve, GroundRollPhase, RotationPhase, RobustRotationPhase, ClimbAnglePhase, SteadyFlightPhase +from .phases import ( + BFLImplicitSolve, + GroundRollPhase, + RotationPhase, + RobustRotationPhase, + ClimbAnglePhase, + SteadyFlightPhase, +) from .mission_groups import TrajectoryGroup + class MissionWithReserve(TrajectoryGroup): """ This analysis group is set up to compute all the major parameters @@ -51,81 +59,192 @@ class MissionWithReserve(TrajectoryGroup): """ def initialize(self): - self.options.declare('num_nodes', default=9, desc="Number of points per phase. Needs to be 2N + 1 due to simpson's rule") - self.options.declare('aircraft_model', default=None, desc="OpenConcept-compliant airplane model") + self.options.declare( + "num_nodes", default=9, desc="Number of points per phase. Needs to be 2N + 1 due to simpson's rule" + ) + self.options.declare("aircraft_model", default=None, desc="OpenConcept-compliant airplane model") def setup(self): - nn = self.options['num_nodes'] - acmodelclass = self.options['aircraft_model'] - - mp = self.add_subsystem('missionparams', om.IndepVarComp(),promotes_outputs=['*']) - mp.add_output('takeoff|h',val=0.,units='ft') - mp.add_output('cruise|h0',val=28000.,units='ft') - mp.add_output('mission_range',val=1250.,units='NM') - mp.add_output('reserve_range',val=200., units='NM') - mp.add_output('reserve|h0', val=25000., units='ft') - mp.add_output('loiter|h0', val=1500., units='ft') - mp.add_output('loiter_duration', val=30.*60., units='s') - mp.add_output('payload',val=1000.,units='lbm') - - # add the climb, cruise, and descent phases - phase1 = self.add_subsystem('climb',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='climb'),promotes_inputs=['ac|*']) - # set the climb time such that the specified initial cruise altitude is exactly reached - phase1.add_subsystem('climbdt',om.BalanceComp(name='duration',units='s',eq_units='m',val=120,upper=2000,lower=0,rhs_name='cruise|h0',lhs_name='fltcond|h_final'),promotes_outputs=['duration']) - phase1.connect('ode_integ_phase.fltcond|h_final','climbdt.fltcond|h_final') - self.connect('cruise|h0', 'climb.climbdt.cruise|h0') - - phase2 = self.add_subsystem('cruise',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='cruise'),promotes_inputs=['ac|*']) - # set the cruise time such that the desired design range is flown by the end of the mission - phase2.add_subsystem('cruisedt',om.BalanceComp(name='duration',units='s',eq_units='m',val=120, upper=25000, lower=0,rhs_name='mission_range',lhs_name='range_final'),promotes_outputs=['duration']) - self.connect('mission_range', 'cruise.cruisedt.mission_range') - phase3 = self.add_subsystem('descent',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='descent'),promotes_inputs=['ac|*']) - # set the descent time so that the final altitude is sea level again - phase3.add_subsystem('descentdt',om.BalanceComp(name='duration',units='s',eq_units='m', val=120, upper=8000, lower=0,rhs_name='takeoff|h',lhs_name='fltcond|h_final'),promotes_outputs=['duration']) - self.connect('descent.ode_integ_phase.range_final','cruise.cruisedt.range_final') - self.connect('takeoff|h', 'descent.descentdt.takeoff|h') - phase3.connect('ode_integ_phase.fltcond|h_final','descentdt.fltcond|h_final') - - # add the climb, cruise, and descent phases for the reserve mission - phase4 = self.add_subsystem('reserve_climb',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='reserve_climb'),promotes_inputs=['ac|*']) - # set the climb time such that the specified initial cruise altitude is exactly reached - phase4.add_subsystem('reserve_climbdt',om.BalanceComp(name='duration',units='s',eq_units='m',val=120,upper=2000,lower=0,rhs_name='reserve|h0',lhs_name='fltcond|h_final'),promotes_outputs=['duration']) - phase4.connect('ode_integ_phase.fltcond|h_final','reserve_climbdt.fltcond|h_final') - self.connect('reserve|h0', 'reserve_climb.reserve_climbdt.reserve|h0') - - phase5 = self.add_subsystem('reserve_cruise',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='reserve_cruise'),promotes_inputs=['ac|*']) - # set the reserve_cruise time such that the desired design range is flown by the end of the mission - phase5.add_subsystem('reserve_cruisedt',om.BalanceComp(name='duration',units='s',eq_units='m',val=120, upper=25000, lower=0,rhs_name='reserve_range',lhs_name='range_final'),promotes_outputs=['duration']) - self.connect('reserve_range', 'reserve_cruise.reserve_cruisedt.reserve_range') - - phase6 = self.add_subsystem('reserve_descent',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='reserve_descent'),promotes_inputs=['ac|*']) - # set the reserve_descent time so that the final altitude is sea level again - phase6.add_subsystem('reserve_descentdt',om.BalanceComp(name='duration',units='s',eq_units='m', val=120, upper=8000, lower=0,rhs_name='takeoff|h',lhs_name='fltcond|h_final'),promotes_outputs=['duration']) - phase6.connect('ode_integ_phase.fltcond|h_final','reserve_descentdt.fltcond|h_final') - self.connect('takeoff|h', 'reserve_descent.reserve_descentdt.takeoff|h') - - reserverange = om.ExecComp('reserverange=rangef-rangeo', - reserverange={'val': 100., 'units': 'NM'}, - rangeo={'val': 0., 'units': 'NM'}, - rangef={'val': 100., 'units': 'NM'}) - self.add_subsystem('resrange', reserverange) - self.connect('descent.ode_integ_phase.range_final', 'resrange.rangeo') - self.connect('reserve_descent.ode_integ_phase.range_final', 'resrange.rangef') - self.connect('resrange.reserverange','reserve_cruise.reserve_cruisedt.range_final') - # self.connect('reserve_descent.range_final', 'reserve_cruisedt.range_final') - - phase7 = self.add_subsystem('loiter',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='loiter'),promotes_inputs=['ac|*']) - dvlist = [['duration_in', 'duration', 300, 's']] - phase7.add_subsystem('loiter_dt', DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) - self.connect('loiter|h0','loiter.ode_integ_phase.fltcond|h_initial') - self.connect('loiter_duration','loiter.duration_in') - - self.link_phases(phase1, phase2) - self.link_phases(phase2, phase3) - self.link_phases(phase3, phase4, states_to_skip=['ode_integ_phase.fltcond|h']) - self.link_phases(phase4, phase5) - self.link_phases(phase5, phase6) - self.link_phases(phase6, phase7, states_to_skip=['ode_integ_phase.fltcond|h']) + nn = self.options["num_nodes"] + acmodelclass = self.options["aircraft_model"] + + mp = self.add_subsystem("missionparams", om.IndepVarComp(), promotes_outputs=["*"]) + mp.add_output("takeoff|h", val=0.0, units="ft") + mp.add_output("cruise|h0", val=28000.0, units="ft") + mp.add_output("mission_range", val=1250.0, units="NM") + mp.add_output("reserve_range", val=200.0, units="NM") + mp.add_output("reserve|h0", val=25000.0, units="ft") + mp.add_output("loiter|h0", val=1500.0, units="ft") + mp.add_output("loiter_duration", val=30.0 * 60.0, units="s") + mp.add_output("payload", val=1000.0, units="lbm") + + # add the climb, cruise, and descent phases + phase1 = self.add_subsystem( + "climb", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="climb"), + promotes_inputs=["ac|*"], + ) + # set the climb time such that the specified initial cruise altitude is exactly reached + phase1.add_subsystem( + "climbdt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=2000, + lower=0, + rhs_name="cruise|h0", + lhs_name="fltcond|h_final", + ), + promotes_outputs=["duration"], + ) + phase1.connect("ode_integ_phase.fltcond|h_final", "climbdt.fltcond|h_final") + self.connect("cruise|h0", "climb.climbdt.cruise|h0") + + phase2 = self.add_subsystem( + "cruise", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="cruise"), + promotes_inputs=["ac|*"], + ) + # set the cruise time such that the desired design range is flown by the end of the mission + phase2.add_subsystem( + "cruisedt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=25000, + lower=0, + rhs_name="mission_range", + lhs_name="range_final", + ), + promotes_outputs=["duration"], + ) + self.connect("mission_range", "cruise.cruisedt.mission_range") + phase3 = self.add_subsystem( + "descent", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="descent"), + promotes_inputs=["ac|*"], + ) + # set the descent time so that the final altitude is sea level again + phase3.add_subsystem( + "descentdt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=8000, + lower=0, + rhs_name="takeoff|h", + lhs_name="fltcond|h_final", + ), + promotes_outputs=["duration"], + ) + self.connect("descent.ode_integ_phase.range_final", "cruise.cruisedt.range_final") + self.connect("takeoff|h", "descent.descentdt.takeoff|h") + phase3.connect("ode_integ_phase.fltcond|h_final", "descentdt.fltcond|h_final") + + # add the climb, cruise, and descent phases for the reserve mission + phase4 = self.add_subsystem( + "reserve_climb", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="reserve_climb"), + promotes_inputs=["ac|*"], + ) + # set the climb time such that the specified initial cruise altitude is exactly reached + phase4.add_subsystem( + "reserve_climbdt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=2000, + lower=0, + rhs_name="reserve|h0", + lhs_name="fltcond|h_final", + ), + promotes_outputs=["duration"], + ) + phase4.connect("ode_integ_phase.fltcond|h_final", "reserve_climbdt.fltcond|h_final") + self.connect("reserve|h0", "reserve_climb.reserve_climbdt.reserve|h0") + + phase5 = self.add_subsystem( + "reserve_cruise", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="reserve_cruise"), + promotes_inputs=["ac|*"], + ) + # set the reserve_cruise time such that the desired design range is flown by the end of the mission + phase5.add_subsystem( + "reserve_cruisedt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=25000, + lower=0, + rhs_name="reserve_range", + lhs_name="range_final", + ), + promotes_outputs=["duration"], + ) + self.connect("reserve_range", "reserve_cruise.reserve_cruisedt.reserve_range") + + phase6 = self.add_subsystem( + "reserve_descent", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="reserve_descent"), + promotes_inputs=["ac|*"], + ) + # set the reserve_descent time so that the final altitude is sea level again + phase6.add_subsystem( + "reserve_descentdt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=8000, + lower=0, + rhs_name="takeoff|h", + lhs_name="fltcond|h_final", + ), + promotes_outputs=["duration"], + ) + phase6.connect("ode_integ_phase.fltcond|h_final", "reserve_descentdt.fltcond|h_final") + self.connect("takeoff|h", "reserve_descent.reserve_descentdt.takeoff|h") + + reserverange = om.ExecComp( + "reserverange=rangef-rangeo", + reserverange={"val": 100.0, "units": "NM"}, + rangeo={"val": 0.0, "units": "NM"}, + rangef={"val": 100.0, "units": "NM"}, + ) + self.add_subsystem("resrange", reserverange) + self.connect("descent.ode_integ_phase.range_final", "resrange.rangeo") + self.connect("reserve_descent.ode_integ_phase.range_final", "resrange.rangef") + self.connect("resrange.reserverange", "reserve_cruise.reserve_cruisedt.range_final") + # self.connect('reserve_descent.range_final', 'reserve_cruisedt.range_final') + + phase7 = self.add_subsystem( + "loiter", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="loiter"), + promotes_inputs=["ac|*"], + ) + dvlist = [["duration_in", "duration", 300, "s"]] + phase7.add_subsystem("loiter_dt", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) + self.connect("loiter|h0", "loiter.ode_integ_phase.fltcond|h_initial") + self.connect("loiter_duration", "loiter.duration_in") + + self.link_phases(phase1, phase2) + self.link_phases(phase2, phase3) + self.link_phases(phase3, phase4, states_to_skip=["ode_integ_phase.fltcond|h"]) + self.link_phases(phase4, phase5) + self.link_phases(phase5, phase6) + self.link_phases(phase6, phase7, states_to_skip=["ode_integ_phase.fltcond|h"]) + class BasicMission(TrajectoryGroup): """ @@ -160,7 +279,7 @@ class BasicMission(TrajectoryGroup): Mission payload (default 1000 lbm) mission_range : float Design range (deault 1250 NM) - + Options ------- aircraft_model : class @@ -170,51 +289,109 @@ class BasicMission(TrajectoryGroup): """ def initialize(self): - self.options.declare('num_nodes', default=9, desc="Number of points per phase. Needs to be 2N + 1 due to simpson's rule") - self.options.declare('aircraft_model', default=None, desc="OpenConcept-compliant airplane model") - self.options.declare('include_ground_roll', default=False, desc='Whether to include groundroll phase') + self.options.declare( + "num_nodes", default=9, desc="Number of points per phase. Needs to be 2N + 1 due to simpson's rule" + ) + self.options.declare("aircraft_model", default=None, desc="OpenConcept-compliant airplane model") + self.options.declare("include_ground_roll", default=False, desc="Whether to include groundroll phase") def setup(self): - nn = self.options['num_nodes'] - acmodelclass = self.options['aircraft_model'] - grflag = self.options['include_ground_roll'] - - mp = self.add_subsystem('missionparams', om.IndepVarComp(),promotes_outputs=['*']) - mp.add_output('takeoff|h',val=0.,units='ft') - mp.add_output('cruise|h0',val=28000.,units='ft') - mp.add_output('mission_range',val=1250.,units='NM') - mp.add_output('payload',val=1000.,units='lbm') - mp.add_output('takeoff|v2', val=150., units='kn') - - if grflag: - mp.add_output('takeoff|v0', val=4.0, units='kn') - phase0 = self.add_subsystem('groundroll', GroundRollPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='v0v1'), promotes_inputs=['ac|*']) - self.connect('takeoff|v2', 'groundroll.takeoff|v1') - - # add the climb, cruise, and descent phases - phase1 = self.add_subsystem('climb',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='climb'),promotes_inputs=['ac|*']) - # set the climb time such that the specified initial cruise altitude is exactly reached - phase1.add_subsystem('climbdt',om.BalanceComp(name='duration',units='s',eq_units='m',val=120,upper=2000,lower=0,rhs_name='cruise|h0',lhs_name='fltcond|h_final'),promotes_outputs=['duration']) - phase1.connect('ode_integ_phase.fltcond|h_final','climbdt.fltcond|h_final') - self.connect('cruise|h0', 'climb.climbdt.cruise|h0') - self.connect('takeoff|h', 'climb.ode_integ_phase.fltcond|h_initial') - - phase2 = self.add_subsystem('cruise',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='cruise'),promotes_inputs=['ac|*']) - # set the cruise time such that the desired design range is flown by the end of the mission - phase2.add_subsystem('cruisedt',om.BalanceComp(name='duration',units='s',eq_units='m',val=120, upper=25000, lower=0,rhs_name='mission_range',lhs_name='range_final'),promotes_outputs=['duration']) - self.connect('mission_range', 'cruise.cruisedt.mission_range') - - phase3 = self.add_subsystem('descent',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='descent'),promotes_inputs=['ac|*']) - # set the descent time so that the final altitude is sea level again - phase3.add_subsystem('descentdt',om.BalanceComp(name='duration',units='s',eq_units='m', val=120, upper=8000, lower=0,rhs_name='takeoff|h',lhs_name='fltcond|h_final'),promotes_outputs=['duration']) - self.connect('descent.ode_integ_phase.range_final','cruise.cruisedt.range_final') - self.connect('takeoff|h', 'descent.descentdt.takeoff|h') - phase3.connect('ode_integ_phase.fltcond|h_final','descentdt.fltcond|h_final') - - if grflag: - self.link_phases(phase0, phase1, states_to_skip=['fltcond|h']) - self.link_phases(phase1, phase2) - self.link_phases(phase2, phase3) + nn = self.options["num_nodes"] + acmodelclass = self.options["aircraft_model"] + grflag = self.options["include_ground_roll"] + + mp = self.add_subsystem("missionparams", om.IndepVarComp(), promotes_outputs=["*"]) + mp.add_output("takeoff|h", val=0.0, units="ft") + mp.add_output("cruise|h0", val=28000.0, units="ft") + mp.add_output("mission_range", val=1250.0, units="NM") + mp.add_output("payload", val=1000.0, units="lbm") + mp.add_output("takeoff|v2", val=150.0, units="kn") + + if grflag: + mp.add_output("takeoff|v0", val=4.0, units="kn") + phase0 = self.add_subsystem( + "groundroll", + GroundRollPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="v0v1"), + promotes_inputs=["ac|*"], + ) + self.connect("takeoff|v2", "groundroll.takeoff|v1") + + # add the climb, cruise, and descent phases + phase1 = self.add_subsystem( + "climb", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="climb"), + promotes_inputs=["ac|*"], + ) + # set the climb time such that the specified initial cruise altitude is exactly reached + phase1.add_subsystem( + "climbdt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=2000, + lower=0, + rhs_name="cruise|h0", + lhs_name="fltcond|h_final", + ), + promotes_outputs=["duration"], + ) + phase1.connect("ode_integ_phase.fltcond|h_final", "climbdt.fltcond|h_final") + self.connect("cruise|h0", "climb.climbdt.cruise|h0") + self.connect("takeoff|h", "climb.ode_integ_phase.fltcond|h_initial") + + phase2 = self.add_subsystem( + "cruise", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="cruise"), + promotes_inputs=["ac|*"], + ) + # set the cruise time such that the desired design range is flown by the end of the mission + phase2.add_subsystem( + "cruisedt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=25000, + lower=0, + rhs_name="mission_range", + lhs_name="range_final", + ), + promotes_outputs=["duration"], + ) + self.connect("mission_range", "cruise.cruisedt.mission_range") + + phase3 = self.add_subsystem( + "descent", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="descent"), + promotes_inputs=["ac|*"], + ) + # set the descent time so that the final altitude is sea level again + phase3.add_subsystem( + "descentdt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + upper=8000, + lower=0, + rhs_name="takeoff|h", + lhs_name="fltcond|h_final", + ), + promotes_outputs=["duration"], + ) + self.connect("descent.ode_integ_phase.range_final", "cruise.cruisedt.range_final") + self.connect("takeoff|h", "descent.descentdt.takeoff|h") + phase3.connect("ode_integ_phase.fltcond|h_final", "descentdt.fltcond|h_final") + + if grflag: + self.link_phases(phase0, phase1, states_to_skip=["fltcond|h"]) + self.link_phases(phase1, phase2) + self.link_phases(phase2, phase3) + class FullMissionAnalysis(TrajectoryGroup): """ @@ -272,72 +449,149 @@ class FullMissionAnalysis(TrajectoryGroup): """ def initialize(self): - self.options.declare('num_nodes', default=9, desc="Number of points per phase. Needs to be 2N + 1 due to simpson's rule") - self.options.declare('aircraft_model', default=None, desc="OpenConcept-compliant airplane model") - self.options.declare('transition_method', default='simplified', desc="Method to use for computing transition") + self.options.declare( + "num_nodes", default=9, desc="Number of points per phase. Needs to be 2N + 1 due to simpson's rule" + ) + self.options.declare("aircraft_model", default=None, desc="OpenConcept-compliant airplane model") + self.options.declare("transition_method", default="simplified", desc="Method to use for computing transition") def setup(self): - nn = self.options['num_nodes'] - acmodelclass = self.options['aircraft_model'] - transition_method = self.options['transition_method'] - - # add the four balanced field length takeoff phases and the implicit v1 solver - # v0v1 - from a rolling start to v1 speed - # v1vr - from the decision speed to rotation - # rotate - in the air following rotation in 2DOF - # v1vr - emergency stopping from v1 to a stop. - - mp = self.add_subsystem('missionparams',om.IndepVarComp(),promotes_outputs=['*']) - mp.add_output('takeoff|h',val=0.,units='ft') - mp.add_output('cruise|h0',val=28000.,units='ft') - mp.add_output('mission_range',val=1250.,units='NM') - mp.add_output('payload',val=1000.,units='lbm') - - self.add_subsystem('bfl', BFLImplicitSolve(), promotes_outputs=['takeoff|v1']) - v0v1 = self.add_subsystem('v0v1', GroundRollPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='v0v1'), promotes_inputs=['ac|*','takeoff|v1']) - v1vr = self.add_subsystem('v1vr', GroundRollPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='v1vr'), promotes_inputs=['ac|*']) - self.connect('takeoff|v1','v1vr.fltcond|Utrue_initial') - self.connect('v0v1.range_final','v1vr.range_initial') - if transition_method == 'simplified': - rotate = self.add_subsystem('rotate',RobustRotationPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='rotate'),promotes_inputs=['ac|*']) - elif transition_method == 'ode': - rotate = self.add_subsystem('rotate',RotationPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='rotate'),promotes_inputs=['ac|*']) - self.connect('v1vr.fltcond|Utrue_final','rotate.fltcond|Utrue_initial') - else: - raise IOError('Invalid option for transition method') - self.connect('v1vr.range_final','rotate.range_initial') - self.connect('rotate.range_final','bfl.distance_continue') - self.connect('v1vr.takeoff|vr','bfl.takeoff|vr') - v1v0 = self.add_subsystem('v1v0',GroundRollPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='v1v0'), promotes_inputs=['ac|*','takeoff|v1']) - self.connect('v0v1.range_final','v1v0.range_initial') - self.connect('v1v0.range_final','bfl.distance_abort') - self.add_subsystem('engineoutclimb',ClimbAnglePhase(num_nodes=1, aircraft_model=acmodelclass, flight_phase='EngineOutClimbAngle'), promotes_inputs=['ac|*']) - - # add the climb, cruise, and descent phases - climb = self.add_subsystem('climb',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='climb'),promotes_inputs=['ac|*']) - # set the climb time such that the specified initial cruise altitude is exactly reached - climb.add_subsystem('climbdt',om.BalanceComp(name='duration',units='s',eq_units='m',val=120,lower=0,upper=3000,rhs_name='cruise|h0',lhs_name='fltcond|h_final'), promotes_outputs=['duration']) - climb.connect('ode_integ_phase.fltcond|h_final','climbdt.fltcond|h_final') - self.connect('cruise|h0', 'climb.climbdt.cruise|h0') - - cruise = self.add_subsystem('cruise',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='cruise'),promotes_inputs=['ac|*']) - # set the cruise time such that the desired design range is flown by the end of the mission - cruise.add_subsystem('cruisedt',om.BalanceComp(name='duration',units='s',eq_units='km',val=120, lower=0,upper=30000,rhs_name='mission_range',lhs_name='range_final'),promotes_outputs=['duration']) - self.connect('mission_range', 'cruise.cruisedt.mission_range') - - descent = self.add_subsystem('descent',SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase='descent'),promotes_inputs=['ac|*']) - # set the descent time so that the final altitude is sea level again - descent.add_subsystem('descentdt',om.BalanceComp(name='duration',units='s',eq_units='m', val=120, lower=0,upper=3000,rhs_name='takeoff|h',lhs_name='fltcond|h_final'),promotes_outputs=['duration']) - self.connect('takeoff|h','descent.descentdt.takeoff|h') - self.connect('descent.ode_integ_phase.range_final','cruise.cruisedt.range_final') - self.connect('descent.ode_integ_phase.fltcond|h_final','descent.descentdt.fltcond|h_final') - - # connect range, fuel burn, and altitude from the end of each phase to the beginning of the next, in order - self.link_phases(v0v1, v1vr, states_to_skip=['fltcond|Utrue','range']) - self.link_phases(v1vr, rotate, states_to_skip=['fltcond|Utrue','range']) - self.link_phases(v0v1, v1v0, states_to_skip=['fltcond|Utrue','range']) - self.link_phases(rotate, climb) - self.link_phases(climb, cruise) - self.link_phases(cruise, descent) - self.connect('rotate.range_final','climb.ode_integ_phase.range_initial') - self.connect('rotate.fltcond|h_final','climb.ode_integ_phase.fltcond|h_initial') \ No newline at end of file + nn = self.options["num_nodes"] + acmodelclass = self.options["aircraft_model"] + transition_method = self.options["transition_method"] + + # add the four balanced field length takeoff phases and the implicit v1 solver + # v0v1 - from a rolling start to v1 speed + # v1vr - from the decision speed to rotation + # rotate - in the air following rotation in 2DOF + # v1vr - emergency stopping from v1 to a stop. + + mp = self.add_subsystem("missionparams", om.IndepVarComp(), promotes_outputs=["*"]) + mp.add_output("takeoff|h", val=0.0, units="ft") + mp.add_output("cruise|h0", val=28000.0, units="ft") + mp.add_output("mission_range", val=1250.0, units="NM") + mp.add_output("payload", val=1000.0, units="lbm") + + self.add_subsystem("bfl", BFLImplicitSolve(), promotes_outputs=["takeoff|v1"]) + v0v1 = self.add_subsystem( + "v0v1", + GroundRollPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="v0v1"), + promotes_inputs=["ac|*", "takeoff|v1"], + ) + v1vr = self.add_subsystem( + "v1vr", + GroundRollPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="v1vr"), + promotes_inputs=["ac|*"], + ) + self.connect("takeoff|v1", "v1vr.fltcond|Utrue_initial") + self.connect("v0v1.range_final", "v1vr.range_initial") + if transition_method == "simplified": + rotate = self.add_subsystem( + "rotate", + RobustRotationPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="rotate"), + promotes_inputs=["ac|*"], + ) + elif transition_method == "ode": + rotate = self.add_subsystem( + "rotate", + RotationPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="rotate"), + promotes_inputs=["ac|*"], + ) + self.connect("v1vr.fltcond|Utrue_final", "rotate.fltcond|Utrue_initial") + else: + raise IOError("Invalid option for transition method") + self.connect("v1vr.range_final", "rotate.range_initial") + self.connect("rotate.range_final", "bfl.distance_continue") + self.connect("v1vr.takeoff|vr", "bfl.takeoff|vr") + v1v0 = self.add_subsystem( + "v1v0", + GroundRollPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="v1v0"), + promotes_inputs=["ac|*", "takeoff|v1"], + ) + self.connect("v0v1.range_final", "v1v0.range_initial") + self.connect("v1v0.range_final", "bfl.distance_abort") + self.add_subsystem( + "engineoutclimb", + ClimbAnglePhase(num_nodes=1, aircraft_model=acmodelclass, flight_phase="EngineOutClimbAngle"), + promotes_inputs=["ac|*"], + ) + + # add the climb, cruise, and descent phases + climb = self.add_subsystem( + "climb", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="climb"), + promotes_inputs=["ac|*"], + ) + # set the climb time such that the specified initial cruise altitude is exactly reached + climb.add_subsystem( + "climbdt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + lower=0, + upper=3000, + rhs_name="cruise|h0", + lhs_name="fltcond|h_final", + ), + promotes_outputs=["duration"], + ) + climb.connect("ode_integ_phase.fltcond|h_final", "climbdt.fltcond|h_final") + self.connect("cruise|h0", "climb.climbdt.cruise|h0") + + cruise = self.add_subsystem( + "cruise", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="cruise"), + promotes_inputs=["ac|*"], + ) + # set the cruise time such that the desired design range is flown by the end of the mission + cruise.add_subsystem( + "cruisedt", + om.BalanceComp( + name="duration", + units="s", + eq_units="km", + val=120, + lower=0, + upper=30000, + rhs_name="mission_range", + lhs_name="range_final", + ), + promotes_outputs=["duration"], + ) + self.connect("mission_range", "cruise.cruisedt.mission_range") + + descent = self.add_subsystem( + "descent", + SteadyFlightPhase(num_nodes=nn, aircraft_model=acmodelclass, flight_phase="descent"), + promotes_inputs=["ac|*"], + ) + # set the descent time so that the final altitude is sea level again + descent.add_subsystem( + "descentdt", + om.BalanceComp( + name="duration", + units="s", + eq_units="m", + val=120, + lower=0, + upper=3000, + rhs_name="takeoff|h", + lhs_name="fltcond|h_final", + ), + promotes_outputs=["duration"], + ) + self.connect("takeoff|h", "descent.descentdt.takeoff|h") + self.connect("descent.ode_integ_phase.range_final", "cruise.cruisedt.range_final") + self.connect("descent.ode_integ_phase.fltcond|h_final", "descent.descentdt.fltcond|h_final") + + # connect range, fuel burn, and altitude from the end of each phase to the beginning of the next, in order + self.link_phases(v0v1, v1vr, states_to_skip=["fltcond|Utrue", "range"]) + self.link_phases(v1vr, rotate, states_to_skip=["fltcond|Utrue", "range"]) + self.link_phases(v0v1, v1v0, states_to_skip=["fltcond|Utrue", "range"]) + self.link_phases(rotate, climb) + self.link_phases(climb, cruise) + self.link_phases(cruise, descent) + self.connect("rotate.range_final", "climb.ode_integ_phase.range_initial") + self.connect("rotate.fltcond|h_final", "climb.ode_integ_phase.fltcond|h_initial") diff --git a/openconcept/mission/tests/test_solver_phase_helpers.py b/openconcept/mission/tests/test_solver_phase_helpers.py index 101c9243..7a77d8e5 100644 --- a/openconcept/mission/tests/test_solver_phase_helpers.py +++ b/openconcept/mission/tests/test_solver_phase_helpers.py @@ -2,23 +2,32 @@ import numpy as np from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials from openmdao.api import IndepVarComp, Group, Problem -from openconcept.mission import ClimbAngleComp, Groundspeeds, HorizontalAcceleration, VerticalAcceleration, SteadyFlightCL, FlipVectorComp, TakeoffTransition +from openconcept.mission import ( + ClimbAngleComp, + Groundspeeds, + HorizontalAcceleration, + VerticalAcceleration, + SteadyFlightCL, + FlipVectorComp, + TakeoffTransition, +) from openconcept.utilities.constants import GRAV_CONST # TESTS FOR ClimbAngleComp =================================== + class ClimbAngleCompTestGroup(Group): def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('conditions', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('thrust', val=np.ones((nn,))*1000, units='N') - iv.add_output('drag', val=np.ones((nn,))*1000, units='N') - iv.add_output('weight', val=np.ones((nn,))*1000, units='kg') + nn = self.options["num_nodes"] + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("thrust", val=np.ones((nn,)) * 1000, units="N") + iv.add_output("drag", val=np.ones((nn,)) * 1000, units="N") + iv.add_output("weight", val=np.ones((nn,)) * 1000, units="kg") - self.add_subsystem('climbangle', ClimbAngleComp(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + self.add_subsystem("climbangle", ClimbAngleComp(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) class ClimbAngleTestCase_Scalar(unittest.TestCase): @@ -28,88 +37,96 @@ def setUp(self): self.prob.run_model() def test_level_flight(self): - assert_near_equal(self.prob['gamma'][0],0,tolerance=1e-10) + assert_near_equal(self.prob["gamma"][0], 0, tolerance=1e-10) def test_climb_flight(self): - self.prob['thrust'] = np.ones((1,))*1200 + self.prob["thrust"] = np.ones((1,)) * 1200 self.prob.run_model() - assert_near_equal(self.prob['gamma'][0], np.arcsin(200 / 1000 / GRAV_CONST), tolerance=1e-10) + assert_near_equal(self.prob["gamma"][0], np.arcsin(200 / 1000 / GRAV_CONST), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + # TESTS FOR FlipVectorComp =================================== + class FlipVectorCompTestGroup(Group): def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of points to run") - self.options.declare('units', default=None) - self.options.declare('negative', default=False) + self.options.declare("num_nodes", default=1, desc="Number of points to run") + self.options.declare("units", default=None) + self.options.declare("negative", default=False) def setup(self): - nn = self.options['num_nodes'] - unit_string = self.options['units'] - neg_flag = self.options['negative'] + nn = self.options["num_nodes"] + unit_string = self.options["units"] + neg_flag = self.options["negative"] - iv = self.add_subsystem('conditions', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('thrust', val=np.linspace(0,100,nn), units='N') + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("thrust", val=np.linspace(0, 100, nn), units="N") + + self.add_subsystem( + "flipvector", FlipVectorComp(num_nodes=nn, units=unit_string, negative=neg_flag), promotes_outputs=["*"] + ) + self.connect("thrust", "flipvector.vec_in") - self.add_subsystem('flipvector', FlipVectorComp(num_nodes=nn, units=unit_string, negative=neg_flag), promotes_outputs=['*']) - self.connect('thrust', 'flipvector.vec_in') class FlipVectorCompTestCase_Vector(unittest.TestCase): def setUp(self): - self.prob = Problem(FlipVectorCompTestGroup(num_nodes=11, units='N')) + self.prob = Problem(FlipVectorCompTestGroup(num_nodes=11, units="N")) self.prob.setup(check=True, force_alloc_complex=True) self.prob.run_model() def test_flip_vec_order(self): - assert_near_equal(self.prob['vec_out'], np.linspace(100, 0, 11), tolerance=1e-10) + assert_near_equal(self.prob["vec_out"], np.linspace(100, 0, 11), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class FlipVectorCompTestCase_Scalar(unittest.TestCase): def setUp(self): - self.prob = Problem(FlipVectorCompTestGroup(num_nodes=1, units='N')) + self.prob = Problem(FlipVectorCompTestGroup(num_nodes=1, units="N")) self.prob.setup(check=True, force_alloc_complex=True) self.prob.run_model() def test_flip_vec_order(self): - assert_near_equal(self.prob['vec_out'], np.zeros((1,)), tolerance=1e-10) + assert_near_equal(self.prob["vec_out"], np.zeros((1,)), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class FlipVectorCompTestCase_Negative(unittest.TestCase): def setUp(self): - self.prob = Problem(FlipVectorCompTestGroup(num_nodes=11, units='N', negative=True)) + self.prob = Problem(FlipVectorCompTestGroup(num_nodes=11, units="N", negative=True)) self.prob.setup(check=True, force_alloc_complex=True) self.prob.run_model() def test_flip_vec_order(self): - assert_near_equal(self.prob['vec_out'], np.linspace(-100, 0, 11), tolerance=1e-10) + assert_near_equal(self.prob["vec_out"], np.linspace(-100, 0, 11), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) # TESTS FOR Groundspeeds =================================== + class GroundspeedsTestGroup(Group): def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('conditions', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('fltcond|vs', val=np.linspace(0,3,nn), units='m/s') - iv.add_output('fltcond|Utrue', val=np.ones((nn,))*50, units='m/s') - self.add_subsystem('gs', Groundspeeds(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + nn = self.options["num_nodes"] + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("fltcond|vs", val=np.linspace(0, 3, nn), units="m/s") + iv.add_output("fltcond|Utrue", val=np.ones((nn,)) * 50, units="m/s") + self.add_subsystem("gs", Groundspeeds(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) class GroundspeedsTestCase(unittest.TestCase): @@ -119,36 +136,39 @@ def setUp(self): self.prob.run_model() def test_level_flight(self): - assert_near_equal(self.prob['fltcond|groundspeed'][0],50,tolerance=1e-10) - assert_near_equal(self.prob['fltcond|cosgamma'][0],1.,tolerance=1e-10) - assert_near_equal(self.prob['fltcond|singamma'][0],0.,tolerance=1e-10) + assert_near_equal(self.prob["fltcond|groundspeed"][0], 50, tolerance=1e-10) + assert_near_equal(self.prob["fltcond|cosgamma"][0], 1.0, tolerance=1e-10) + assert_near_equal(self.prob["fltcond|singamma"][0], 0.0, tolerance=1e-10) def test_climb_flight(self): gs = np.sqrt(50**2 - 3**2) - assert_near_equal(self.prob['fltcond|groundspeed'][-1],gs,tolerance=1e-10) - assert_near_equal(self.prob['fltcond|cosgamma'][-1],gs/50.,tolerance=1e-10) - assert_near_equal(self.prob['fltcond|singamma'][-1],3./50.,tolerance=1e-10) + assert_near_equal(self.prob["fltcond|groundspeed"][-1], gs, tolerance=1e-10) + assert_near_equal(self.prob["fltcond|cosgamma"][-1], gs / 50.0, tolerance=1e-10) + assert_near_equal(self.prob["fltcond|singamma"][-1], 3.0 / 50.0, tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + # TESTS FOR HorizontalAcceleration =================================== + class HorizontalAccelerationTestGroup(Group): def initialize(self): - self.options.declare('num_nodes',default=9,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=9, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('conditions', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('weight', val=np.ones((nn,))*100, units='kg') - iv.add_output('lift', val=np.ones((nn,))*100, units='N') - iv.add_output('thrust', val=np.ones((nn,))*100, units='N') - iv.add_output('drag', val=np.ones((nn,))*100, units='N') - iv.add_output('fltcond|singamma', val=np.zeros((nn,)), units=None) - iv.add_output('braking', val=np.zeros((nn,)), units=None) - self.add_subsystem('ha', HorizontalAcceleration(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + nn = self.options["num_nodes"] + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("weight", val=np.ones((nn,)) * 100, units="kg") + iv.add_output("lift", val=np.ones((nn,)) * 100, units="N") + iv.add_output("thrust", val=np.ones((nn,)) * 100, units="N") + iv.add_output("drag", val=np.ones((nn,)) * 100, units="N") + iv.add_output("fltcond|singamma", val=np.zeros((nn,)), units=None) + iv.add_output("braking", val=np.zeros((nn,)), units=None) + self.add_subsystem("ha", HorizontalAcceleration(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + class HorizontalAccelerationTestCase_SteadyLevel(unittest.TestCase): def setUp(self): @@ -157,49 +177,52 @@ def setUp(self): self.prob.run_model() def test_steady_level_flights(self): - assert_near_equal(self.prob['accel_horiz'], np.zeros((9,)), tolerance=1e-10) + assert_near_equal(self.prob["accel_horiz"], np.zeros((9,)), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class HorizontalAccelerationTestCase_SteadyClimb(unittest.TestCase): def setUp(self): self.prob = Problem(HorizontalAccelerationTestGroup(num_nodes=9)) self.prob.setup(check=True, force_alloc_complex=True) - self.prob['thrust'] = np.ones((9,)) * (100 + 100 * GRAV_CONST * 0.02) - self.prob['fltcond|singamma'] = np.ones((9,)) * 0.02 + self.prob["thrust"] = np.ones((9,)) * (100 + 100 * GRAV_CONST * 0.02) + self.prob["fltcond|singamma"] = np.ones((9,)) * 0.02 self.prob.run_model() def test_steady_climb_flights(self): - assert_near_equal(self.prob['accel_horiz'], np.zeros((9,)), tolerance=1e-10) + assert_near_equal(self.prob["accel_horiz"], np.zeros((9,)), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class HorizontalAccelerationTestCase_SteadyClimb(unittest.TestCase): def setUp(self): self.prob = Problem(HorizontalAccelerationTestGroup(num_nodes=9)) self.prob.setup(check=True, force_alloc_complex=True) - self.prob['thrust'] = np.ones((9,)) * (100 + 100 * GRAV_CONST * 0.02) - self.prob['fltcond|singamma'] = np.ones((9,)) * 0.02 + self.prob["thrust"] = np.ones((9,)) * (100 + 100 * GRAV_CONST * 0.02) + self.prob["fltcond|singamma"] = np.ones((9,)) * 0.02 self.prob.run_model() def test_steady_climb_flights(self): - assert_near_equal(self.prob['accel_horiz'], np.zeros((9,)), tolerance=1e-10) + assert_near_equal(self.prob["accel_horiz"], np.zeros((9,)), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class HorizontalAccelerationTestCase_UnsteadyRunwayAccel(unittest.TestCase): def setUp(self): self.prob = Problem(HorizontalAccelerationTestGroup(num_nodes=9)) self.prob.setup(check=True, force_alloc_complex=True) - self.prob['braking'] = np.ones((9,)) * 0.03 - self.prob['lift'] = np.linspace(0, 150, 9) * GRAV_CONST - self.prob['drag'] = np.ones((9,)) * 50 + self.prob["braking"] = np.ones((9,)) * 0.03 + self.prob["lift"] = np.linspace(0, 150, 9) * GRAV_CONST + self.prob["drag"] = np.ones((9,)) * 50 self.prob.run_model() def test_accel_with_braking(self): @@ -207,57 +230,60 @@ def test_accel_with_braking(self): thrust = 100.0 lift = 0.0 mass = 100 - weight = mass*GRAV_CONST + weight = mass * GRAV_CONST singamma = 0.0 - brakeforce = 0.03 * (weight-lift) + brakeforce = 0.03 * (weight - lift) slopeforce = weight * singamma accel_horz_actual = (thrust - drag - brakeforce - slopeforce) / mass - assert_near_equal(self.prob['accel_horiz'][0], accel_horz_actual, tolerance=1e-10) + assert_near_equal(self.prob["accel_horiz"][0], accel_horz_actual, tolerance=1e-10) def test_accel_with_braking_and_lift(self): drag = 50.0 thrust = 100.0 mass = 100 - weight = mass*GRAV_CONST + weight = mass * GRAV_CONST singamma = 0.0 - lift = weight*0.75 - brakeforce = 0.03 * (weight-lift) + lift = weight * 0.75 + brakeforce = 0.03 * (weight - lift) slopeforce = weight * singamma accel_horz_actual = (thrust - drag - brakeforce - slopeforce) / mass - assert_near_equal(self.prob['accel_horiz'][4], accel_horz_actual, tolerance=1e-10) + assert_near_equal(self.prob["accel_horiz"][4], accel_horz_actual, tolerance=1e-10) def test_accel_lift_exceeds_weight(self): drag = 50.0 thrust = 100.0 mass = 100 - weight = mass*GRAV_CONST + weight = mass * GRAV_CONST singamma = 0.0 # if lift exceeds weight (as it does here) no braking force is applied brakeforce = 0.0 slopeforce = weight * singamma accel_horz_actual = (thrust - drag - brakeforce - slopeforce) / mass - assert_near_equal(self.prob['accel_horiz'][-1], accel_horz_actual, tolerance=1e-10) + assert_near_equal(self.prob["accel_horiz"][-1], accel_horz_actual, tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + # TESTS FOR VerticalAcceleration =================================== + class VerticalAccelerationTestGroup(Group): def initialize(self): - self.options.declare('num_nodes',default=9,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=9, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('conditions', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('weight', val=np.ones((nn,))*100, units='kg') - iv.add_output('lift', val=np.ones((nn,))*100*GRAV_CONST, units='N') - iv.add_output('thrust', val=np.ones((nn,))*100, units='N') - iv.add_output('drag', val=np.ones((nn,))*100, units='N') - iv.add_output('fltcond|singamma', val=np.zeros((nn,)), units=None) - iv.add_output('fltcond|cosgamma', val=np.ones((nn,)), units=None) - self.add_subsystem('va', VerticalAcceleration(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + nn = self.options["num_nodes"] + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("weight", val=np.ones((nn,)) * 100, units="kg") + iv.add_output("lift", val=np.ones((nn,)) * 100 * GRAV_CONST, units="N") + iv.add_output("thrust", val=np.ones((nn,)) * 100, units="N") + iv.add_output("drag", val=np.ones((nn,)) * 100, units="N") + iv.add_output("fltcond|singamma", val=np.zeros((nn,)), units=None) + iv.add_output("fltcond|cosgamma", val=np.ones((nn,)), units=None) + self.add_subsystem("va", VerticalAcceleration(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + class VerticalAccelerationTestCase_SteadyLevel(unittest.TestCase): def setUp(self): @@ -266,57 +292,62 @@ def setUp(self): self.prob.run_model() def test_steady_level_flights(self): - assert_near_equal(self.prob['accel_vert'], np.zeros((9,)), tolerance=1e-10) + assert_near_equal(self.prob["accel_vert"], np.zeros((9,)), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class VerticalAccelerationTestCase_SteadyClimbing(unittest.TestCase): def setUp(self): self.prob = Problem(VerticalAccelerationTestGroup(num_nodes=9)) self.prob.setup(check=True, force_alloc_complex=True) - self.prob['fltcond|singamma'] = np.ones((9,)) * np.sin(0.02) - self.prob['fltcond|cosgamma'] = np.ones((9,)) * np.cos(0.02) - self.prob['lift'] = np.ones((9,)) * 100 * GRAV_CONST / np.cos(0.02) + self.prob["fltcond|singamma"] = np.ones((9,)) * np.sin(0.02) + self.prob["fltcond|cosgamma"] = np.ones((9,)) * np.cos(0.02) + self.prob["lift"] = np.ones((9,)) * 100 * GRAV_CONST / np.cos(0.02) self.prob.run_model() def test_steady_climbing_flight(self): - assert_near_equal(self.prob['accel_vert'], np.zeros((9,)), tolerance=1e-10) + assert_near_equal(self.prob["accel_vert"], np.zeros((9,)), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class VerticalAccelerationTestCase_UnsteadyPullUp(unittest.TestCase): def setUp(self): self.prob = Problem(VerticalAccelerationTestGroup(num_nodes=9)) self.prob.setup(check=True, force_alloc_complex=True) - self.prob['lift'] = np.ones((9,)) * 100 * GRAV_CONST + 100 + self.prob["lift"] = np.ones((9,)) * 100 * GRAV_CONST + 100 self.prob.run_model() def test_unsteady_pullup(self): - assert_near_equal(self.prob['accel_vert'], (100./100.)*np.ones((9,)), tolerance=1e-10) + assert_near_equal(self.prob["accel_vert"], (100.0 / 100.0) * np.ones((9,)), tolerance=1e-10) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + # TESTS FOR SteadyFlightCL =================================== + class SteadyFlightCLTestGroup(Group): def initialize(self): - self.options.declare('num_nodes',default=9,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=9, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('conditions', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('weight', val=np.ones((nn,))*100, units='kg') - iv.add_output('fltcond|q', val=np.ones((nn,))*1000, units='Pa') - iv.add_output('ac|geom|wing|S_ref', val=10, units='m**2') - iv.add_output('fltcond|cosgamma', val=np.ones((nn,)), units=None) - self.add_subsystem('cls', SteadyFlightCL(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + nn = self.options["num_nodes"] + iv = self.add_subsystem("conditions", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("weight", val=np.ones((nn,)) * 100, units="kg") + iv.add_output("fltcond|q", val=np.ones((nn,)) * 1000, units="Pa") + iv.add_output("ac|geom|wing|S_ref", val=10, units="m**2") + iv.add_output("fltcond|cosgamma", val=np.ones((nn,)), units=None) + self.add_subsystem("cls", SteadyFlightCL(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + class SteadyFlightCLTestCase_Level(unittest.TestCase): def setUp(self): @@ -325,24 +356,30 @@ def setUp(self): self.prob.run_model() def test_steady_level_flights(self): - assert_near_equal(self.prob['fltcond|CL'], np.ones((9,))*100*GRAV_CONST/1000./10./1.0, tolerance=1e-10) + assert_near_equal( + self.prob["fltcond|CL"], np.ones((9,)) * 100 * GRAV_CONST / 1000.0 / 10.0 / 1.0, tolerance=1e-10 + ) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class SteadyFlightCLTestCase_Climb(unittest.TestCase): def setUp(self): self.prob = Problem(SteadyFlightCLTestGroup(num_nodes=9)) self.prob.setup(check=True, force_alloc_complex=True) - self.prob['fltcond|cosgamma'] = 0.98*np.ones((9,)) + self.prob["fltcond|cosgamma"] = 0.98 * np.ones((9,)) self.prob.run_model() def test_steady_level_flights(self): - assert_near_equal(self.prob['fltcond|CL'], np.ones((9,))*100*GRAV_CONST/1000./10.*0.98, tolerance=1e-10) + assert_near_equal( + self.prob["fltcond|CL"], np.ones((9,)) * 100 * GRAV_CONST / 1000.0 / 10.0 * 0.98, tolerance=1e-10 + ) def test_partials(self): - partials = self.prob.check_partials(method='cs', out_stream=None) + partials = self.prob.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -# TODO add TakeoffTransition and TakeoffClimb component unit tests \ No newline at end of file + +# TODO add TakeoffTransition and TakeoffClimb component unit tests diff --git a/openconcept/mission/tests/test_trajectories.py b/openconcept/mission/tests/test_trajectories.py index 70418ef9..232220c4 100644 --- a/openconcept/mission/tests/test_trajectories.py +++ b/openconcept/mission/tests/test_trajectories.py @@ -31,8 +31,8 @@ class TestForDocs(unittest.TestCase): def test_for_docs(self): prob = self.trajectory_example() - assert_near_equal(prob['phase2.vm.ode_integ.velocity_final'], 1.66689857, 1e-8) - + assert_near_equal(prob["phase2.vm.ode_integ.velocity_final"], 1.66689857, 1e-8) + def trajectory_example(self): """ A simple example illustrating the auto-integration feature in OpenConcept @@ -44,42 +44,50 @@ def trajectory_example(self): class NewtonSecondLaw(om.ExplicitComponent): "A regular OpenMDAO component computing acceleration from mass and force" + def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - num_nodes = self.options['num_nodes'] - self.add_input('mass', val=2.0*np.ones((num_nodes,)), units='kg') - self.add_input('force', val=1.0*np.ones((num_nodes,)), units='N') + num_nodes = self.options["num_nodes"] + self.add_input("mass", val=2.0 * np.ones((num_nodes,)), units="kg") + self.add_input("force", val=1.0 * np.ones((num_nodes,)), units="N") # mark the output variable for integration using openmdao tags - self.add_output('accel', val=0.5*np.ones((num_nodes,)), - units='m/s**2', tags=['integrate', - 'state_name:velocity', - 'state_units:m/s', - 'state_val:5.0', - 'state_promotes:True']) - self.declare_partials(['*'], ['*'], method='cs') + self.add_output( + "accel", + val=0.5 * np.ones((num_nodes,)), + units="m/s**2", + tags=[ + "integrate", + "state_name:velocity", + "state_units:m/s", + "state_val:5.0", + "state_promotes:True", + ], + ) + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - outputs['accel'] = inputs['force'] / inputs['mass'] + outputs["accel"] = inputs["force"] / inputs["mass"] class DragEquation(om.ExplicitComponent): "Another regular OpenMDAO component that happens to take a state variable as input" + def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - num_nodes = self.options['num_nodes'] - self.add_input('velocity', val=0.0*np.ones((num_nodes,)), units='m/s') - self.add_output('force', val=0.0*np.ones((num_nodes,)), units='N') - self.declare_partials(['*'], ['*'], method='cs') + num_nodes = self.options["num_nodes"] + self.add_input("velocity", val=0.0 * np.ones((num_nodes,)), units="m/s") + self.add_output("force", val=0.0 * np.ones((num_nodes,)), units="N") + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - outputs['force'] = -0.10 * inputs['velocity'] ** 2 + outputs["force"] = -0.10 * inputs["velocity"] ** 2 class VehicleModel(IntegratorGroup): """ - A user wishing to integrate an ODE rate will need to subclass + A user wishing to integrate an ODE rate will need to subclass this IntegratorGroup instead of the default OpenMDAO Group but it behaves in all other ways exactly the same. @@ -87,75 +95,89 @@ class VehicleModel(IntegratorGroup): using the regular Group. Only the direct parent of the rate to be integrated has to use this special class. """ + def initialize(self): - self.options.declare('num_nodes', default=11) + self.options.declare("num_nodes", default=11) def setup(self): - num_nodes = self.options['num_nodes'] - self.add_subsystem('nsl', NewtonSecondLaw(num_nodes=num_nodes)) - self.add_subsystem('drag', DragEquation(num_nodes=num_nodes)) + num_nodes = self.options["num_nodes"] + self.add_subsystem("nsl", NewtonSecondLaw(num_nodes=num_nodes)) + self.add_subsystem("drag", DragEquation(num_nodes=num_nodes)) # velocity output is created automatically by the integrator # if you want you can promote it, or you can connect it directly as here - self.connect('velocity', 'drag.velocity') - self.connect('drag.force','nsl.force') + self.connect("velocity", "drag.velocity") + self.connect("drag.force", "nsl.force") class MyPhase(PhaseGroup): "An OpenConcept Phase comprises part of a time-based TrajectoryGroup and always needs to have a 'duration' defined" + def setup(self): - self.add_subsystem('ivc', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['duration']) - self.add_subsystem('vm', VehicleModel(time_units='min', num_nodes=self.options['num_nodes'])) + self.add_subsystem( + "ivc", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["duration"] + ) + self.add_subsystem("vm", VehicleModel(time_units="min", num_nodes=self.options["num_nodes"])) class MyTraj(TrajectoryGroup): "An OpenConcept TrajectoryGroup consists of one or more phases that may be linked together. This will often be a top-level model" + def setup(self): - self.add_subsystem('phase1', MyPhase(num_nodes=11)) - self.add_subsystem('phase2', MyPhase(num_nodes=11)) + self.add_subsystem("phase1", MyPhase(num_nodes=11)) + self.add_subsystem("phase2", MyPhase(num_nodes=11)) # the link_phases directive ensures continuity of state variables across phase boundaries self.link_phases(self.phase1, self.phase2) prob = om.Problem(MyTraj()) prob.model.nonlinear_solver = om.NewtonSolver(iprint=2) prob.model.linear_solver = om.DirectSolver() - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.nonlinear_solver.options['maxiter'] = 20 - prob.model.nonlinear_solver.options['atol'] = 1e-6 - prob.model.nonlinear_solver.options['rtol'] = 1e-6 + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.nonlinear_solver.options["maxiter"] = 20 + prob.model.nonlinear_solver.options["atol"] = 1e-6 + prob.model.nonlinear_solver.options["rtol"] = 1e-6 prob.setup() # set the initial value of the state at the beginning of the TrajectoryGroup - prob['phase1.vm.ode_integ.velocity_initial'] = 10.0 + prob["phase1.vm.ode_integ.velocity_initial"] = 10.0 prob.run_model() prob.model.list_outputs(print_arrays=True, units=True) prob.model.list_inputs(print_arrays=True, units=True) - + return prob + # ============== IntegratorGroup Tests ========== # + class IntegratorGroupTestBase(IntegratorGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('iv', om.IndepVarComp('x', val=np.linspace(0, 5, nn), units='s')) - ec = self.add_subsystem('ec', om.ExecComp(['df = -10.2*x**2 + 4.2*x -10.5'], - df={'val': 1.0*np.ones((nn,)), - 'units': 'kg/s', - 'tags': ['integrate', 'state_name:f', 'state_units:kg']}, - x={'val': 1.0*np.ones((nn,)), - 'units': 's'})) - self.connect('iv.x', 'ec.x') - self.set_order(['iv', 'ec', 'ode_integ']) + nn = self.options["num_nodes"] + iv = self.add_subsystem("iv", om.IndepVarComp("x", val=np.linspace(0, 5, nn), units="s")) + ec = self.add_subsystem( + "ec", + om.ExecComp( + ["df = -10.2*x**2 + 4.2*x -10.5"], + df={ + "val": 1.0 * np.ones((nn,)), + "units": "kg/s", + "tags": ["integrate", "state_name:f", "state_units:kg"], + }, + x={"val": 1.0 * np.ones((nn,)), "units": "s"}, + ), + ) + self.connect("iv.x", "ec.x") + self.set_order(["iv", "ec", "ode_integ"]) + class TestIntegratorSingleState(unittest.TestCase): class TestPhase(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - self.add_subsystem('ic', IntegratorGroupTestBase(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + self.add_subsystem("ic", IntegratorGroupTestBase(num_nodes=nn)) def setUp(self): self.nn = 5 @@ -165,39 +187,47 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - assert_near_equal(self.p['ic.ode_integ.f'], f_exact) - self.p['ic.ode_integ.f_initial'] = -2.0 + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + assert_near_equal(self.p["ic.ode_integ.f"], f_exact) + self.p["ic.ode_integ.f_initial"] = -2.0 self.p.run_model() - assert_near_equal(self.p['ic.ode_integ.f'], f_exact-2.0) + assert_near_equal(self.p["ic.ode_integ.f"], f_exact - 2.0) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class IntegratorTestMultipleOutputs(IntegratorGroupTestBase): def setup(self): super(IntegratorTestMultipleOutputs, self).setup() - nn = self.options['num_nodes'] - ec2 = self.add_subsystem('ec2', om.ExecComp(['df2 = 5.1*x**2 +0.5*x-7.2'], - df2={'val': 1.0*np.ones((nn,)), - 'units': 'W', - 'tags': ['integrate', 'state_name:f2', 'state_units:J']}, - x={'val': 1.0*np.ones((nn,)), - 'units': 's'})) - self.connect('iv.x', 'ec2.x') - self.set_order(['iv', 'ec', 'ec2', 'ode_integ']) + nn = self.options["num_nodes"] + ec2 = self.add_subsystem( + "ec2", + om.ExecComp( + ["df2 = 5.1*x**2 +0.5*x-7.2"], + df2={ + "val": 1.0 * np.ones((nn,)), + "units": "W", + "tags": ["integrate", "state_name:f2", "state_units:J"], + }, + x={"val": 1.0 * np.ones((nn,)), "units": "s"}, + ), + ) + self.connect("iv.x", "ec2.x") + self.set_order(["iv", "ec", "ec2", "ode_integ"]) + class TestIntegratorMultipleState(unittest.TestCase): class TestPhase(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - self.add_subsystem('ic', IntegratorTestMultipleOutputs(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + self.add_subsystem("ic", IntegratorTestMultipleOutputs(num_nodes=nn)) def setUp(self): self.nn = 5 @@ -207,43 +237,51 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x - assert_near_equal(self.p['ic.ode_integ.f'], f_exact) - assert_near_equal(self.p['ic.ode_integ.f2'], f2_exact) - self.p['ic.ode_integ.f_initial'] = -4.3 - self.p['ic.ode_integ.f2_initial'] = 85.1 + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x + assert_near_equal(self.p["ic.ode_integ.f"], f_exact) + assert_near_equal(self.p["ic.ode_integ.f2"], f2_exact) + self.p["ic.ode_integ.f_initial"] = -4.3 + self.p["ic.ode_integ.f2_initial"] = 85.1 self.p.run_model() - assert_near_equal(self.p['ic.ode_integ.f'], f_exact - 4.3) - assert_near_equal(self.p['ic.ode_integ.f2'], f2_exact + 85.1) + assert_near_equal(self.p["ic.ode_integ.f"], f_exact - 4.3) + assert_near_equal(self.p["ic.ode_integ.f2"], f2_exact + 85.1) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class IntegratorTestPromotes(IntegratorGroupTestBase): def setup(self): super(IntegratorTestPromotes, self).setup() - nn = self.options['num_nodes'] - ec2 = self.add_subsystem('ec2', om.ExecComp(['df2 = 5.1*x**2 +0.5*x-7.2'], - df2={'val': 1.0*np.ones((nn,)), - 'units': 'W', - 'tags': ['integrate', 'state_name:f2', 'state_units:J', 'state_promotes:True']}, - x={'val': 1.0*np.ones((nn,)), - 'units': 's'})) - self.connect('iv.x', 'ec2.x') - self.set_order(['iv', 'ec', 'ec2', 'ode_integ']) + nn = self.options["num_nodes"] + ec2 = self.add_subsystem( + "ec2", + om.ExecComp( + ["df2 = 5.1*x**2 +0.5*x-7.2"], + df2={ + "val": 1.0 * np.ones((nn,)), + "units": "W", + "tags": ["integrate", "state_name:f2", "state_units:J", "state_promotes:True"], + }, + x={"val": 1.0 * np.ones((nn,)), "units": "s"}, + ), + ) + self.connect("iv.x", "ec2.x") + self.set_order(["iv", "ec", "ec2", "ode_integ"]) + class TestIntegratorPromotes(unittest.TestCase): class TestPhase(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - self.add_subsystem('ic', IntegratorTestPromotes(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + self.add_subsystem("ic", IntegratorTestPromotes(num_nodes=nn)) def setUp(self): self.nn = 5 @@ -253,44 +291,58 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x - assert_near_equal(self.p['ic.ode_integ.f'], f_exact) - assert_near_equal(self.p['ic.f2'], f2_exact) - self.p['ic.ode_integ.f_initial'] = -4.3 - self.p['ic.ode_integ.f2_initial'] = 85.1 - self.p.run_model() - assert_near_equal(self.p['ic.ode_integ.f'], f_exact - 4.3) - assert_near_equal(self.p['ic.f2'], f2_exact + 85.1) - + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x + assert_near_equal(self.p["ic.ode_integ.f"], f_exact) + assert_near_equal(self.p["ic.f2"], f2_exact) + self.p["ic.ode_integ.f_initial"] = -4.3 + self.p["ic.ode_integ.f2_initial"] = 85.1 + self.p.run_model() + assert_near_equal(self.p["ic.ode_integ.f"], f_exact - 4.3) + assert_near_equal(self.p["ic.f2"], f2_exact + 85.1) + def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class IntegratorTestValLimits(IntegratorGroupTestBase): def setup(self): super(IntegratorTestValLimits, self).setup() - nn = self.options['num_nodes'] - ec2 = self.add_subsystem('ec2', om.ExecComp(['df2 = 5.1*x**2 +0.5*x-7.2'], - df2={'val': 1.0*np.ones((nn,)), - 'units': 'W', - 'tags': ['integrate', 'state_name:f2', 'state_units:J', - 'state_upper:1e20', 'state_lower:-1e20', 'state_val:np.linspace(0,5,'+str(nn)+')']}, - x={'val': 1.0*np.ones((nn,)), - 'units': 's'})) - self.connect('iv.x', 'ec2.x') - self.set_order(['iv', 'ec', 'ec2', 'ode_integ']) + nn = self.options["num_nodes"] + ec2 = self.add_subsystem( + "ec2", + om.ExecComp( + ["df2 = 5.1*x**2 +0.5*x-7.2"], + df2={ + "val": 1.0 * np.ones((nn,)), + "units": "W", + "tags": [ + "integrate", + "state_name:f2", + "state_units:J", + "state_upper:1e20", + "state_lower:-1e20", + "state_val:np.linspace(0,5," + str(nn) + ")", + ], + }, + x={"val": 1.0 * np.ones((nn,)), "units": "s"}, + ), + ) + self.connect("iv.x", "ec2.x") + self.set_order(["iv", "ec", "ec2", "ode_integ"]) + class TestIntegratorValLimits(unittest.TestCase): class TestPhase(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - self.add_subsystem('ic', IntegratorTestValLimits(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + self.add_subsystem("ic", IntegratorTestValLimits(num_nodes=nn)) def setUp(self): self.nn = 5 @@ -298,124 +350,137 @@ def setUp(self): self.p.setup(force_alloc_complex=True) def test_results(self): - assert_near_equal(self.p['ic.ode_integ.f'], 0.0*np.ones((self.nn,))) - assert_near_equal(self.p['ic.ode_integ.f2'], np.linspace(0, 5, self.nn)) + assert_near_equal(self.p["ic.ode_integ.f"], 0.0 * np.ones((self.nn,))) + assert_near_equal(self.p["ic.ode_integ.f2"], np.linspace(0, 5, self.nn)) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestIntegratorMultipleStateSingleNode(TestIntegratorMultipleState): +class TestIntegratorMultipleStateSingleNode(TestIntegratorMultipleState): def setUp(self): self.nn = 1 self.p = om.Problem(model=self.TestPhase(num_nodes=self.nn)) self.p.setup(force_alloc_complex=True) + class IntegratorTestDuplicateRateNames(IntegratorGroupTestBase): def setup(self): super(IntegratorTestDuplicateRateNames, self).setup() - nn = self.options['num_nodes'] - ec2 = self.add_subsystem('ec2', om.ExecComp(['df = 5.1*x**3 +0.5*x-7.2'], - df={'val': 1.0*np.ones((nn,)), - 'units': 'W', - 'tags': ['integrate', 'state_name:f2', 'state_units:J']}, - x={'val': 1.0*np.ones((nn,)), - 'units': 's'})) - self.connect('iv.x', 'ec2.x') - self.set_order(['iv', 'ec', 'ec2', 'ode_integ']) + nn = self.options["num_nodes"] + ec2 = self.add_subsystem( + "ec2", + om.ExecComp( + ["df = 5.1*x**3 +0.5*x-7.2"], + df={"val": 1.0 * np.ones((nn,)), "units": "W", "tags": ["integrate", "state_name:f2", "state_units:J"]}, + x={"val": 1.0 * np.ones((nn,)), "units": "s"}, + ), + ) + self.connect("iv.x", "ec2.x") + self.set_order(["iv", "ec", "ec2", "ode_integ"]) + class TestIntegratorDuplicateRateName(unittest.TestCase): class TestPhase(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - self.add_subsystem('ic', IntegratorTestDuplicateRateNames(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + self.add_subsystem("ic", IntegratorTestDuplicateRateNames(num_nodes=nn)) def setUp(self): self.nn = 5 self.p = om.Problem(model=self.TestPhase(num_nodes=self.nn)) - + def test_asserts(self): with self.assertRaises(ValueError) as cm: self.p.setup(force_alloc_complex=True) + class IntegratorTestDuplicateStateNames(IntegratorGroupTestBase): def setup(self): super(IntegratorTestDuplicateStateNames, self).setup() - nn = self.options['num_nodes'] - ec2 = self.add_subsystem('ec2', om.ExecComp(['df2 = 5.1*x**3 +0.5*x-7.2'], - df2={'val': 1.0*np.ones((nn,)), - 'units': 'W', - 'tags': ['integrate', 'state_name:f', 'state_units:J']}, - x={'val': 1.0*np.ones((nn,)), - 'units': 's'})) - self.connect('iv.x', 'ec2.x') - self.set_order(['iv', 'ec', 'ec2', 'ode_integ']) + nn = self.options["num_nodes"] + ec2 = self.add_subsystem( + "ec2", + om.ExecComp( + ["df2 = 5.1*x**3 +0.5*x-7.2"], + df2={"val": 1.0 * np.ones((nn,)), "units": "W", "tags": ["integrate", "state_name:f", "state_units:J"]}, + x={"val": 1.0 * np.ones((nn,)), "units": "s"}, + ), + ) + self.connect("iv.x", "ec2.x") + self.set_order(["iv", "ec", "ec2", "ode_integ"]) + class TestIntegratorDuplicateStateName(unittest.TestCase): class TestPhase(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - self.add_subsystem('ic', IntegratorTestDuplicateStateNames(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + self.add_subsystem("ic", IntegratorTestDuplicateStateNames(num_nodes=nn)) def setUp(self): self.nn = 5 self.p = om.Problem(model=self.TestPhase(num_nodes=self.nn)) - + def test_asserts(self): with self.assertRaises(ValueError) as cm: self.p.setup(force_alloc_complex=True) - self.assertIn("Variable name 'f_final' already exists.", '{}'.format(cm.exception)) + self.assertIn("Variable name 'f_final' already exists.", "{}".format(cm.exception)) + class TestIntegratorOutsideofPhase(unittest.TestCase): def setUp(self): self.nn = 5 self.p = om.Problem(model=IntegratorGroupTestBase(num_nodes=self.nn)) - + def test_asserts(self): with self.assertRaises(NameError) as cm: self.p.setup(force_alloc_complex=True) - self.assertEqual('{}'.format(cm.exception), - 'Integrator group must be created within an OpenConcept phase or Dymos trajectory') + self.assertEqual( + "{}".format(cm.exception), + "Integrator group must be created within an OpenConcept phase or Dymos trajectory", + ) + class TestIntegratorNoIntegratedState(unittest.TestCase): def setUp(self): self.nn = 5 grp = IntegratorGroup() - grp.add_subsystem('iv', om.IndepVarComp('a', val=1.0)) + grp.add_subsystem("iv", om.IndepVarComp("a", val=1.0)) phase = PhaseGroup() - phase.add_subsystem('iv', om.IndepVarComp('duration', val=3.0, units='s'), promotes_outputs=['*']) - phase.add_subsystem('grp', grp) + phase.add_subsystem("iv", om.IndepVarComp("duration", val=3.0, units="s"), promotes_outputs=["*"]) + phase.add_subsystem("grp", grp) self.p = om.Problem(model=phase) self.p.setup() - + def test_runs(self): self.p.run_model() + class IntegratorGroupWithGroup(IntegratorGroupTestBase): def setup(self): super(IntegratorGroupWithGroup, self).setup() - self.add_subsystem('group', om.Group()) + self.add_subsystem("group", om.Group()) -class TestIntegratorWithGroup(unittest.TestCase): +class TestIntegratorWithGroup(unittest.TestCase): class TestPhase(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - self.add_subsystem('ic', IntegratorGroupWithGroup(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + self.add_subsystem("ic", IntegratorGroupWithGroup(num_nodes=nn)) def setUp(self): self.nn = 5 @@ -425,43 +490,51 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - assert_near_equal(self.p['ic.ode_integ.f'], f_exact) - self.p['ic.ode_integ.f_initial'] = -2.0 + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + assert_near_equal(self.p["ic.ode_integ.f"], f_exact) + self.p["ic.ode_integ.f_initial"] = -2.0 self.p.run_model() - assert_near_equal(self.p['ic.ode_integ.f'], f_exact-2.0) + assert_near_equal(self.p["ic.ode_integ.f"], f_exact - 2.0) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) class IntegratorGroupTestPromotedRate(IntegratorGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - iv = self.add_subsystem('iv', om.IndepVarComp('x', val=np.linspace(0, 5, nn), units='s')) - ec = self.add_subsystem('ec', om.ExecComp(['df = -10.2*x**2 + 4.2*x -10.5'], - df={'val': 1.0*np.ones((nn,)), - 'units': 'kg/s', - 'tags': ['integrate', 'state_name:f', 'state_units:kg']}, - x={'val': 1.0*np.ones((nn,)), - 'units': 's'}), promotes_outputs=['df']) - self.connect('iv.x', 'ec.x') - self.set_order(['iv', 'ec', 'ode_integ']) + nn = self.options["num_nodes"] + iv = self.add_subsystem("iv", om.IndepVarComp("x", val=np.linspace(0, 5, nn), units="s")) + ec = self.add_subsystem( + "ec", + om.ExecComp( + ["df = -10.2*x**2 + 4.2*x -10.5"], + df={ + "val": 1.0 * np.ones((nn,)), + "units": "kg/s", + "tags": ["integrate", "state_name:f", "state_units:kg"], + }, + x={"val": 1.0 * np.ones((nn,)), "units": "s"}, + ), + promotes_outputs=["df"], + ) + self.connect("iv.x", "ec.x") + self.set_order(["iv", "ec", "ode_integ"]) + class TestIntegratorSingleStatePromotedRate(unittest.TestCase): class TestPhase(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - self.add_subsystem('ic', IntegratorGroupTestPromotedRate(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + self.add_subsystem("ic", IntegratorGroupTestPromotedRate(num_nodes=nn)) def setUp(self): self.nn = 5 @@ -471,43 +544,46 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - assert_near_equal(self.p['ic.ode_integ.f'], f_exact) - self.p['ic.ode_integ.f_initial'] = -2.0 + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + assert_near_equal(self.p["ic.ode_integ.f"], f_exact) + self.p["ic.ode_integ.f_initial"] = -2.0 self.p.run_model() - assert_near_equal(self.p['ic.ode_integ.f'], f_exact-2.0) + assert_near_equal(self.p["ic.ode_integ.f"], f_exact - 2.0) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + # ============== PhaseGroup Tests ========== # + class TestPhaseNoTime(unittest.TestCase): def setUp(self): self.nn = 5 grp = IntegratorGroup() - grp.add_subsystem('iv', om.IndepVarComp('a', val=1.0)) + grp.add_subsystem("iv", om.IndepVarComp("a", val=1.0)) phase = PhaseGroup() - phase.add_subsystem('grp', grp) + phase.add_subsystem("grp", grp) self.p = om.Problem(model=phase) - + def test_raises_error(self): with self.assertRaises(NameError) as x: self.p.setup() + class TestPhaseMultipleIntegrators(unittest.TestCase): def setUp(self): self.nn = 5 grp1 = IntegratorGroupTestBase(num_nodes=self.nn) grp2 = om.Group() - grp2a = grp2.add_subsystem('a', IntegratorGroupTestBase(num_nodes=self.nn)) - grp2b = grp2.add_subsystem('b', IntegratorGroupTestBase(num_nodes=self.nn)) + grp2a = grp2.add_subsystem("a", IntegratorGroupTestBase(num_nodes=self.nn)) + grp2b = grp2.add_subsystem("b", IntegratorGroupTestBase(num_nodes=self.nn)) phase = PhaseGroup(num_nodes=self.nn) - phase.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - phase.add_subsystem('grp1', grp1) - phase.add_subsystem('grp2', grp2) + phase.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + phase.add_subsystem("grp1", grp1) + phase.add_subsystem("grp2", grp2) self.p = om.Problem(model=phase) self.p.setup(force_alloc_complex=True) @@ -515,31 +591,34 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - assert_near_equal(self.p['grp1.ode_integ.f'], f_exact) - assert_near_equal(self.p['grp2.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['grp2.b.ode_integ.f'], f_exact) - self.p['grp2.a.ode_integ.f_initial'] = -2.0 + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + assert_near_equal(self.p["grp1.ode_integ.f"], f_exact) + assert_near_equal(self.p["grp2.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["grp2.b.ode_integ.f"], f_exact) + self.p["grp2.a.ode_integ.f_initial"] = -2.0 self.p.run_model() - assert_near_equal(self.p['grp2.a.ode_integ.f'], f_exact-2.0) + assert_near_equal(self.p["grp2.a.ode_integ.f"], f_exact - 2.0) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestPhasePromotedDurationVariable(unittest.TestCase): def setUp(self): self.nn = 5 grp1 = IntegratorGroupTestBase(num_nodes=self.nn) grp2 = om.Group() - grp2a = grp2.add_subsystem('a', IntegratorGroupTestBase(num_nodes=self.nn)) - grp2b = grp2.add_subsystem('b', IntegratorGroupTestBase(num_nodes=self.nn)) + grp2a = grp2.add_subsystem("a", IntegratorGroupTestBase(num_nodes=self.nn)) + grp2b = grp2.add_subsystem("b", IntegratorGroupTestBase(num_nodes=self.nn)) phase = PhaseGroup(num_nodes=self.nn) - phase.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - phase.add_subsystem('grp1', grp1) - phase.add_subsystem('grp2', grp2) - phase.add_subsystem('c', om.ExecComp('result = 1.0*duration', duration={'units':'s'}), promotes_inputs=['duration']) + phase.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + phase.add_subsystem("grp1", grp1) + phase.add_subsystem("grp2", grp2) + phase.add_subsystem( + "c", om.ExecComp("result = 1.0*duration", duration={"units": "s"}), promotes_inputs=["duration"] + ) self.p = om.Problem(model=phase) self.p.setup(force_alloc_complex=True) @@ -547,62 +626,72 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - assert_near_equal(self.p['grp1.ode_integ.f'], f_exact) - assert_near_equal(self.p['grp2.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['grp2.b.ode_integ.f'], f_exact) - self.p['grp2.a.ode_integ.f_initial'] = -2.0 + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + assert_near_equal(self.p["grp1.ode_integ.f"], f_exact) + assert_near_equal(self.p["grp2.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["grp2.b.ode_integ.f"], f_exact) + self.p["grp2.a.ode_integ.f_initial"] = -2.0 self.p.run_model() - assert_near_equal(self.p['grp2.a.ode_integ.f'], f_exact-2.0) + assert_near_equal(self.p["grp2.a.ode_integ.f"], f_exact - 2.0) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) # ============ Trajectory Tests ============ # + class PhaseForTrajTest(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) - + self.options.declare("num_nodes", default=1) + def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - a = self.add_subsystem('a', IntegratorGroupTestBase(num_nodes=nn)) - b = self.add_subsystem('b', IntegratorTestMultipleOutputs(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + a = self.add_subsystem("a", IntegratorGroupTestBase(num_nodes=nn)) + b = self.add_subsystem("b", IntegratorTestMultipleOutputs(num_nodes=nn)) + class PhaseForTrajTestWithPromotion(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) - + self.options.declare("num_nodes", default=1) + def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - a = self.add_subsystem('a', IntegratorGroupTestBase(num_nodes=nn)) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + a = self.add_subsystem("a", IntegratorGroupTestBase(num_nodes=nn)) # promote the outputs of b - b = self.add_subsystem('b', IntegratorTestMultipleOutputs(num_nodes=nn), promotes_outputs=['*f2*'], promotes_inputs=['*df2']) + b = self.add_subsystem( + "b", IntegratorTestMultipleOutputs(num_nodes=nn), promotes_outputs=["*f2*"], promotes_inputs=["*df2"] + ) + class PhaseForTrajTestWithPromotionNamesCollide(PhaseGroup): def initialize(self): - self.options.declare('num_nodes', default=1) - + self.options.declare("num_nodes", default=1) + def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('iv', om.IndepVarComp('duration', val=5.0, units='s'), promotes_outputs=['*']) - a = self.add_subsystem('a', IntegratorGroupTestBase(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + nn = self.options["num_nodes"] + self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) + a = self.add_subsystem( + "a", IntegratorGroupTestBase(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) # promote the outputs of b - b = self.add_subsystem('b', IntegratorTestMultipleOutputs(num_nodes=nn), promotes_outputs=['*f2*'], promotes_inputs=['*df2']) + b = self.add_subsystem( + "b", IntegratorTestMultipleOutputs(num_nodes=nn), promotes_outputs=["*f2*"], promotes_inputs=["*df2"] + ) + class TestTrajectoryAllPhaseConnect(unittest.TestCase): def setUp(self): self.nn = 5 traj = TrajectoryGroup() - - phase1 = traj.add_subsystem('phase1', PhaseForTrajTest(num_nodes=5)) - phase2 = traj.add_subsystem('phase2', PhaseForTrajTest(num_nodes=5)) - phase3 = traj.add_subsystem('phase3', PhaseForTrajTest(num_nodes=5)) + + phase1 = traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) + phase2 = traj.add_subsystem("phase2", PhaseForTrajTest(num_nodes=5)) + phase3 = traj.add_subsystem("phase3", PhaseForTrajTest(num_nodes=5)) traj.link_phases(phase1, phase2) traj.link_phases(phase2, phase3) @@ -613,37 +702,38 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x # check first phase result - assert_near_equal(self.p['phase1.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase1.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f2"], f2_exact) # check second phase result - assert_near_equal(self.p['phase2.a.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.b.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.b.ode_integ.f2'], f2_exact+f2_exact[-1]) + assert_near_equal(self.p["phase2.a.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.b.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.b.ode_integ.f2"], f2_exact + f2_exact[-1]) # check third phase result - assert_near_equal(self.p['phase3.a.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.b.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.b.ode_integ.f2'], f2_exact+2.0*f2_exact[-1]) + assert_near_equal(self.p["phase3.a.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.b.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.b.ode_integ.f2"], f2_exact + 2.0 * f2_exact[-1]) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestTrajectoryAllPhaseConnectWithVarPromotion(unittest.TestCase): def setUp(self): self.nn = 5 traj = TrajectoryGroup() - - phase1 = traj.add_subsystem('phase1', PhaseForTrajTestWithPromotion(num_nodes=5)) - phase2 = traj.add_subsystem('phase2', PhaseForTrajTestWithPromotion(num_nodes=5)) - phase3 = traj.add_subsystem('phase3', PhaseForTrajTestWithPromotion(num_nodes=5)) + + phase1 = traj.add_subsystem("phase1", PhaseForTrajTestWithPromotion(num_nodes=5)) + phase2 = traj.add_subsystem("phase2", PhaseForTrajTestWithPromotion(num_nodes=5)) + phase3 = traj.add_subsystem("phase3", PhaseForTrajTestWithPromotion(num_nodes=5)) traj.link_phases(phase1, phase2) traj.link_phases(phase2, phase3) @@ -654,39 +744,40 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x # check first phase result - assert_near_equal(self.p['phase1.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase1.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.ode_integ.f2"], f2_exact) # check second phase result - assert_near_equal(self.p['phase2.a.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.b.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.ode_integ.f2'], f2_exact+f2_exact[-1]) + assert_near_equal(self.p["phase2.a.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.b.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.ode_integ.f2"], f2_exact + f2_exact[-1]) # check third phase result - assert_near_equal(self.p['phase3.a.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.b.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.ode_integ.f2'], f2_exact+2.0*f2_exact[-1]) + assert_near_equal(self.p["phase3.a.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.b.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.ode_integ.f2"], f2_exact + 2.0 * f2_exact[-1]) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestTrajectorySkipPromotedVar(unittest.TestCase): def setUp(self): self.nn = 5 traj = TrajectoryGroup() - - phase1 = traj.add_subsystem('phase1', PhaseForTrajTestWithPromotion(num_nodes=5)) - phase2 = traj.add_subsystem('phase2', PhaseForTrajTestWithPromotion(num_nodes=5)) - phase3 = traj.add_subsystem('phase3', PhaseForTrajTestWithPromotion(num_nodes=5)) - traj.link_phases(phase1, phase2, states_to_skip=['ode_integ.f2']) + phase1 = traj.add_subsystem("phase1", PhaseForTrajTestWithPromotion(num_nodes=5)) + phase2 = traj.add_subsystem("phase2", PhaseForTrajTestWithPromotion(num_nodes=5)) + phase3 = traj.add_subsystem("phase3", PhaseForTrajTestWithPromotion(num_nodes=5)) + + traj.link_phases(phase1, phase2, states_to_skip=["ode_integ.f2"]) traj.link_phases(phase2, phase3) self.p = om.Problem(model=traj) @@ -695,39 +786,40 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x # check first phase result - assert_near_equal(self.p['phase1.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase1.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.ode_integ.f2"], f2_exact) # check second phase result - assert_near_equal(self.p['phase2.a.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.b.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase2.a.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.b.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.ode_integ.f2"], f2_exact) # check third phase result - assert_near_equal(self.p['phase3.a.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.b.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.ode_integ.f2'], f2_exact+f2_exact[-1]) + assert_near_equal(self.p["phase3.a.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.b.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.ode_integ.f2"], f2_exact + f2_exact[-1]) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestTrajectoryAllPhaseConnectWithVarPromotionODEIntegCollide(unittest.TestCase): # This checks for the situation when multiple integrator comps are promoting up ode_integ to the phase level # When this happens duplicate connections can occur def setUp(self): self.nn = 5 traj = TrajectoryGroup() - - phase1 = traj.add_subsystem('phase1', PhaseForTrajTestWithPromotionNamesCollide(num_nodes=5)) - phase2 = traj.add_subsystem('phase2', PhaseForTrajTestWithPromotionNamesCollide(num_nodes=5)) - phase3 = traj.add_subsystem('phase3', PhaseForTrajTestWithPromotionNamesCollide(num_nodes=5)) + + phase1 = traj.add_subsystem("phase1", PhaseForTrajTestWithPromotionNamesCollide(num_nodes=5)) + phase2 = traj.add_subsystem("phase2", PhaseForTrajTestWithPromotionNamesCollide(num_nodes=5)) + phase3 = traj.add_subsystem("phase3", PhaseForTrajTestWithPromotionNamesCollide(num_nodes=5)) traj.link_phases(phase1, phase2) traj.link_phases(phase2, phase3) @@ -738,37 +830,38 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x # check first phase result - assert_near_equal(self.p['phase1.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase1.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.ode_integ.f2"], f2_exact) # check second phase result - assert_near_equal(self.p['phase2.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.b.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.ode_integ.f2'], f2_exact+f2_exact[-1]) + assert_near_equal(self.p["phase2.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.b.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.ode_integ.f2"], f2_exact + f2_exact[-1]) # check third phase result - assert_near_equal(self.p['phase3.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.b.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.ode_integ.f2'], f2_exact+2.0*f2_exact[-1]) + assert_near_equal(self.p["phase3.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.b.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.ode_integ.f2"], f2_exact + 2.0 * f2_exact[-1]) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestTrajectoryTwoPhaseConnect(unittest.TestCase): def setUp(self): self.nn = 5 traj = TrajectoryGroup() - - phase1 = traj.add_subsystem('phase1', PhaseForTrajTest(num_nodes=5)) - phase2 = traj.add_subsystem('phase2', PhaseForTrajTest(num_nodes=5)) - phase3 = traj.add_subsystem('phase3', PhaseForTrajTest(num_nodes=5)) + + phase1 = traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) + phase2 = traj.add_subsystem("phase2", PhaseForTrajTest(num_nodes=5)) + phase3 = traj.add_subsystem("phase3", PhaseForTrajTest(num_nodes=5)) traj.link_phases(phase1, phase2) @@ -778,35 +871,36 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x # check first phase result - assert_near_equal(self.p['phase1.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase1.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f2"], f2_exact) # check second phase result - assert_near_equal(self.p['phase2.a.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.b.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.b.ode_integ.f2'], f2_exact+f2_exact[-1]) + assert_near_equal(self.p["phase2.a.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.b.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.b.ode_integ.f2"], f2_exact + f2_exact[-1]) # check third phase result - assert_near_equal(self.p['phase3.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase3.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase3.b.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase3.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase3.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase3.b.ode_integ.f2"], f2_exact) + class TestTrajectorySkipState(unittest.TestCase): def setUp(self): self.nn = 5 traj = TrajectoryGroup() - - phase1 = traj.add_subsystem('phase1', PhaseForTrajTest(num_nodes=5)) - phase2 = traj.add_subsystem('phase2', PhaseForTrajTest(num_nodes=5)) - phase3 = traj.add_subsystem('phase3', PhaseForTrajTest(num_nodes=5)) - traj.link_phases(phase1, phase2, states_to_skip=['b.ode_integ.f']) - traj.link_phases(phase2, phase3, states_to_skip=['b.ode_integ.f2']) + phase1 = traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) + phase2 = traj.add_subsystem("phase2", PhaseForTrajTest(num_nodes=5)) + phase3 = traj.add_subsystem("phase3", PhaseForTrajTest(num_nodes=5)) + + traj.link_phases(phase1, phase2, states_to_skip=["b.ode_integ.f"]) + traj.link_phases(phase2, phase3, states_to_skip=["b.ode_integ.f2"]) self.p = om.Problem(model=traj) self.p.setup(force_alloc_complex=True) @@ -814,77 +908,80 @@ def setUp(self): def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x # check first phase result - assert_near_equal(self.p['phase1.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase1.b.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase1.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase1.b.ode_integ.f2"], f2_exact) # check second phase result - assert_near_equal(self.p['phase2.a.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['phase2.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['phase2.b.ode_integ.f2'], f2_exact+f2_exact[-1]) + assert_near_equal(self.p["phase2.a.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["phase2.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["phase2.b.ode_integ.f2"], f2_exact + f2_exact[-1]) # check third phase result - assert_near_equal(self.p['phase3.a.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['phase3.b.ode_integ.f'], f_exact+1.0*f_exact[-1]) - assert_near_equal(self.p['phase3.b.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["phase3.a.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.b.ode_integ.f"], f_exact + 1.0 * f_exact[-1]) + assert_near_equal(self.p["phase3.b.ode_integ.f2"], f2_exact) + class TestTrajectoryLinkPhaseStrings(unittest.TestCase): def test_raises(self): self.nn = 5 traj = TrajectoryGroup() - - phase1 = traj.add_subsystem('phase1', PhaseForTrajTest(num_nodes=5)) + + phase1 = traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) with self.assertRaises(ValueError) as context: - traj.link_phases('phase1', 'phase2', states_to_skip=['b.ode_integ.f']) + traj.link_phases("phase1", "phase2", states_to_skip=["b.ode_integ.f"]) + class TestBuryTrajectoryOneLevelDown(unittest.TestCase): def setUp(self): self.nn = 5 traj = TrajectoryGroup() - - phase1 = traj.add_subsystem('phase1', PhaseForTrajTest(num_nodes=5)) - phase2 = traj.add_subsystem('phase2', PhaseForTrajTest(num_nodes=5)) - phase3 = traj.add_subsystem('phase3', PhaseForTrajTest(num_nodes=5)) + + phase1 = traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) + phase2 = traj.add_subsystem("phase2", PhaseForTrajTest(num_nodes=5)) + phase3 = traj.add_subsystem("phase3", PhaseForTrajTest(num_nodes=5)) traj.link_phases(phase1, phase2) traj.link_phases(phase2, phase3) topgroup = om.Group() - topgroup.add_subsystem('traj', traj) + topgroup.add_subsystem("traj", traj) self.p = om.Problem(model=topgroup) self.p.setup(force_alloc_complex=True) def test_results(self): self.p.run_model() x = np.linspace(0, 5, self.nn) - f_exact = -10.2*x**3/3 + 4.2*x**2/2 -10.5*x - f2_exact = 5.1*x**3/3 +0.5*x**2/2-7.2*x + f_exact = -10.2 * x**3 / 3 + 4.2 * x**2 / 2 - 10.5 * x + f2_exact = 5.1 * x**3 / 3 + 0.5 * x**2 / 2 - 7.2 * x # check first phase result - assert_near_equal(self.p['traj.phase1.a.ode_integ.f'], f_exact) - assert_near_equal(self.p['traj.phase1.b.ode_integ.f'], f_exact) - assert_near_equal(self.p['traj.phase1.b.ode_integ.f2'], f2_exact) + assert_near_equal(self.p["traj.phase1.a.ode_integ.f"], f_exact) + assert_near_equal(self.p["traj.phase1.b.ode_integ.f"], f_exact) + assert_near_equal(self.p["traj.phase1.b.ode_integ.f2"], f2_exact) # check second phase result - assert_near_equal(self.p['traj.phase2.a.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['traj.phase2.b.ode_integ.f'], f_exact+f_exact[-1]) - assert_near_equal(self.p['traj.phase2.b.ode_integ.f2'], f2_exact+f2_exact[-1]) + assert_near_equal(self.p["traj.phase2.a.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["traj.phase2.b.ode_integ.f"], f_exact + f_exact[-1]) + assert_near_equal(self.p["traj.phase2.b.ode_integ.f2"], f2_exact + f2_exact[-1]) # check third phase result - assert_near_equal(self.p['traj.phase3.a.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['traj.phase3.b.ode_integ.f'], f_exact+2.0*f_exact[-1]) - assert_near_equal(self.p['traj.phase3.b.ode_integ.f2'], f2_exact+2.0*f2_exact[-1]) + assert_near_equal(self.p["traj.phase3.a.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["traj.phase3.b.ode_integ.f"], f_exact + 2.0 * f_exact[-1]) + assert_near_equal(self.p["traj.phase3.b.ode_integ.f2"], f2_exact + 2.0 * f2_exact[-1]) def test_partials(self): self.p.run_model() - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + # TODO test promoted skipped states if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/openconcept/propulsion/N3.py b/openconcept/propulsion/N3.py index bd28d3a0..021954a6 100644 --- a/openconcept/propulsion/N3.py +++ b/openconcept/propulsion/N3.py @@ -2,6 +2,7 @@ import openmdao.api as om import openconcept + def N3Hybrid(num_nodes=1, plot=False): """ Returns OpenMDAO component for thrust deck @@ -38,19 +39,19 @@ def N3Hybrid(num_nodes=1, plot=False): num_nodes : int Number of analysis points to run (sets vec length; default 1) """ - file_root = openconcept.__path__[0] + r'/propulsion/empirical_data/n+3_hybrid/' - thrustdata = np.load(file_root + r'/power_off/thrust.npy') - fuelburndata_0 = np.load(file_root + r'/power_off/wf.npy') - smwdata_0 = np.load(file_root + r'/power_off/SMW.npy') + file_root = openconcept.__path__[0] + r"/propulsion/empirical_data/n+3_hybrid/" + thrustdata = np.load(file_root + r"/power_off/thrust.npy") + fuelburndata_0 = np.load(file_root + r"/power_off/wf.npy") + smwdata_0 = np.load(file_root + r"/power_off/SMW.npy") - fuelburndata_500 = np.load(file_root + r'/power_on_500kW/wf.npy') - smwdata_500 = np.load(file_root + r'/power_on_500kW/SMW.npy') - fuelburndata_1000 = np.load(file_root + r'/power_on_1MW/wf.npy') - smwdata_1000 = np.load(file_root + r'/power_on_1MW/SMW.npy') + fuelburndata_500 = np.load(file_root + r"/power_on_500kW/wf.npy") + smwdata_500 = np.load(file_root + r"/power_on_500kW/SMW.npy") + fuelburndata_1000 = np.load(file_root + r"/power_on_1MW/wf.npy") + smwdata_1000 = np.load(file_root + r"/power_on_1MW/SMW.npy") - altdata = np.load(file_root + r'/power_off/alt.npy') - machdata = np.load(file_root + r'/power_off/mach.npy') - throttledata = np.load(file_root + r'/power_off/throttle.npy') + altdata = np.load(file_root + r"/power_off/alt.npy") + machdata = np.load(file_root + r"/power_off/mach.npy") + throttledata = np.load(file_root + r"/power_off/throttle.npy") krigedata = [] # do the base case @@ -59,26 +60,38 @@ def N3Hybrid(num_nodes=1, plot=False): for kthrot in range(11): fuelburnijk = fuelburndata_0[ialt, jmach, kthrot] if fuelburnijk > 0.0: - krigedata.append(np.array([throttledata[ialt, jmach, kthrot].copy(), - altdata[ialt, jmach, kthrot].copy(), - machdata[ialt, jmach ,kthrot].copy(), - 0.0, - thrustdata[ialt, jmach, kthrot].copy(), - fuelburnijk.copy(), - smwdata_0[ialt, jmach, kthrot].copy()])) + krigedata.append( + np.array( + [ + throttledata[ialt, jmach, kthrot].copy(), + altdata[ialt, jmach, kthrot].copy(), + machdata[ialt, jmach, kthrot].copy(), + 0.0, + thrustdata[ialt, jmach, kthrot].copy(), + fuelburnijk.copy(), + smwdata_0[ialt, jmach, kthrot].copy(), + ] + ) + ) # do the 500kW case for ialt in range(8): for jmach in range(7): for kthrot in range(11): fuelburnijk = fuelburndata_500[ialt, jmach, kthrot] if fuelburnijk > 0.0: - krigedata.append(np.array([throttledata[ialt, jmach, kthrot].copy(), - altdata[ialt, jmach, kthrot].copy(), - machdata[ialt, jmach ,kthrot].copy(), - 500.0, - thrustdata[ialt, jmach, kthrot].copy(), - fuelburnijk.copy(), - smwdata_500[ialt, jmach, kthrot].copy()])) + krigedata.append( + np.array( + [ + throttledata[ialt, jmach, kthrot].copy(), + altdata[ialt, jmach, kthrot].copy(), + machdata[ialt, jmach, kthrot].copy(), + 500.0, + thrustdata[ialt, jmach, kthrot].copy(), + fuelburnijk.copy(), + smwdata_500[ialt, jmach, kthrot].copy(), + ] + ) + ) # do the 1MW case for ialt in range(8): @@ -86,107 +99,126 @@ def N3Hybrid(num_nodes=1, plot=False): for kthrot in range(11): fuelburnijk = fuelburndata_1000[ialt, jmach, kthrot] if fuelburnijk > 0.0: - krigedata.append(np.array([throttledata[ialt, jmach, kthrot].copy(), - altdata[ialt, jmach, kthrot].copy(), - machdata[ialt, jmach ,kthrot].copy(), - 1000.0, - thrustdata[ialt, jmach, kthrot].copy(), - fuelburnijk.copy(), - smwdata_1000[ialt, jmach, kthrot].copy()])) + krigedata.append( + np.array( + [ + throttledata[ialt, jmach, kthrot].copy(), + altdata[ialt, jmach, kthrot].copy(), + machdata[ialt, jmach, kthrot].copy(), + 1000.0, + thrustdata[ialt, jmach, kthrot].copy(), + fuelburnijk.copy(), + smwdata_1000[ialt, jmach, kthrot].copy(), + ] + ) + ) a = np.array(krigedata) comp = om.MetaModelUnStructuredComp(vec_size=num_nodes) - comp.add_input('throttle', np.ones((num_nodes,))*1., training_data=a[:,0], units=None) - comp.add_input('fltcond|h', np.ones((num_nodes,))*0., training_data=a[:,1], units='ft') - comp.add_input('fltcond|M', np.ones((num_nodes,))*0.3, training_data=a[:,2], units=None) - comp.add_input('hybrid_power', np.zeros((num_nodes,)), training_data=a[:,3], units='kW') - - comp.add_output('thrust', np.ones((num_nodes,))*10000., - training_data=a[:,4], units='lbf', - surrogate=om.KrigingSurrogate(training_cache=file_root+r'n3_hybrid_thrust_trained.zip')) - comp.add_output('fuel_flow', np.ones((num_nodes,))*3.0, - training_data=a[:,5], units='lbm/s', - surrogate=om.KrigingSurrogate(training_cache=file_root+r'n3_hybrid_fuelflow_trained.zip')) - comp.add_output('surge_margin', np.ones((num_nodes,))*3.0, - training_data=a[:,6], units=None, - surrogate=om.KrigingSurrogate(training_cache=file_root+r'n3_hybrid_smw_trained.zip')) - comp.options['default_surrogate'] = om.KrigingSurrogate(lapack_driver='gesvd') + comp.add_input("throttle", np.ones((num_nodes,)) * 1.0, training_data=a[:, 0], units=None) + comp.add_input("fltcond|h", np.ones((num_nodes,)) * 0.0, training_data=a[:, 1], units="ft") + comp.add_input("fltcond|M", np.ones((num_nodes,)) * 0.3, training_data=a[:, 2], units=None) + comp.add_input("hybrid_power", np.zeros((num_nodes,)), training_data=a[:, 3], units="kW") + + comp.add_output( + "thrust", + np.ones((num_nodes,)) * 10000.0, + training_data=a[:, 4], + units="lbf", + surrogate=om.KrigingSurrogate(training_cache=file_root + r"n3_hybrid_thrust_trained.zip"), + ) + comp.add_output( + "fuel_flow", + np.ones((num_nodes,)) * 3.0, + training_data=a[:, 5], + units="lbm/s", + surrogate=om.KrigingSurrogate(training_cache=file_root + r"n3_hybrid_fuelflow_trained.zip"), + ) + comp.add_output( + "surge_margin", + np.ones((num_nodes,)) * 3.0, + training_data=a[:, 6], + units=None, + surrogate=om.KrigingSurrogate(training_cache=file_root + r"n3_hybrid_smw_trained.zip"), + ) + comp.options["default_surrogate"] = om.KrigingSurrogate(lapack_driver="gesvd") if plot: import matplotlib.pyplot as plt + prob = om.Problem() - prob.model.add_subsystem('comp', comp) + prob.model.add_subsystem("comp", comp) prob.setup() machs = np.linspace(0.2, 0.8, 25) - alts = np.linspace(0.0, 35000., 25) + alts = np.linspace(0.0, 35000.0, 25) machs, alts = np.meshgrid(machs, alts) pred = np.zeros((25, 25, 3)) for i in range(25): for j in range(25): - prob.set_val('comp.hybrid_power', 1000., 'kW') - prob['comp.throttle'] = 1.0 - prob['comp.fltcond|h'] = alts[i,j] - prob['comp.fltcond|M'] = machs[i,j] + prob.set_val("comp.hybrid_power", 1000.0, "kW") + prob["comp.throttle"] = 1.0 + prob["comp.fltcond|h"] = alts[i, j] + prob["comp.fltcond|M"] = machs[i, j] prob.run_model() - pred[i,j,0] = prob['comp.thrust'][0].copy() - pred[i,j,1] = prob['comp.fuel_flow'][0].copy() + pred[i, j, 0] = prob["comp.thrust"][0].copy() + pred[i, j, 1] = prob["comp.fuel_flow"][0].copy() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('SFC (lb / hr lb) OM') + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("SFC (lb / hr lb) OM") # plt.contourf(machs, alts, pred[:,:,0]) - plt.contourf(machs, alts, (pred[:,:,1] / pred[:,:,0])*60*60) + plt.contourf(machs, alts, (pred[:, :, 1] / pred[:, :, 0]) * 60 * 60) plt.colorbar() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('Fuel Flow (lb/s)') + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("Fuel Flow (lb/s)") # plt.contourf(machs, alts, pred[:,:,0]) - plt.contourf(machs, alts, pred[:,:,1], levels=20) + plt.contourf(machs, alts, pred[:, :, 1], levels=20) plt.colorbar() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('Thrust (lb)') + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("Thrust (lb)") # plt.contourf(machs, alts, pred[:,:,0]) - plt.contourf(machs, alts, pred[:,:,0], levels=20) + plt.contourf(machs, alts, pred[:, :, 0], levels=20) plt.colorbar() plt.show() throttles = np.linspace(0.1, 1.0, 25) - alts = np.linspace(0.0, 35000., 25) + alts = np.linspace(0.0, 35000.0, 25) throttles, alts = np.meshgrid(throttles, alts) pred = np.zeros((25, 25, 3)) for i in range(25): for j in range(25): - prob.set_val('comp.hybrid_power', 0., 'kW') - prob['comp.throttle'] = throttles[i,j] - prob['comp.fltcond|h'] = alts[i,j] - prob['comp.fltcond|M'] = 0.5 + prob.set_val("comp.hybrid_power", 0.0, "kW") + prob["comp.throttle"] = throttles[i, j] + prob["comp.fltcond|h"] = alts[i, j] + prob["comp.fltcond|M"] = 0.5 prob.run_model() - pred[i,j,0] = prob['comp.thrust'][0].copy() - pred[i,j,1] = prob['comp.fuel_flow'][0].copy() + pred[i, j, 0] = prob["comp.thrust"][0].copy() + pred[i, j, 1] = prob["comp.fuel_flow"][0].copy() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('SFC (lb / hr lb) OM') + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("SFC (lb / hr lb) OM") # plt.contourf(machs, alts, pred[:,:,0]) - plt.contourf(throttles, alts, (pred[:,:,1] / pred[:,:,0])*60*60) + plt.contourf(throttles, alts, (pred[:, :, 1] / pred[:, :, 0]) * 60 * 60) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('Fuel Flow (lb/s)') + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("Fuel Flow (lb/s)") # plt.contourf(throttles, alts, pred[:,:,0]) - plt.contourf(throttles, alts, pred[:,:,1], levels=20) + plt.contourf(throttles, alts, pred[:, :, 1], levels=20) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('Thrust (lb)') + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("Thrust (lb)") # plt.contourf(throttles, alts, pred[:,:,0]) - plt.contourf(throttles, alts, pred[:,:,0], levels=20) + plt.contourf(throttles, alts, pred[:, :, 0], levels=20) plt.colorbar() plt.show() @@ -196,45 +228,46 @@ def N3Hybrid(num_nodes=1, plot=False): pred = np.zeros((25, 25, 3)) for i in range(25): for j in range(25): - prob['comp.hybrid_power'] = powers[i,j] - prob['comp.throttle'] = throttles[i,j] - prob.set_val('comp.fltcond|h', 33000.0, units='ft') - prob['comp.fltcond|M'] = 0.8 + prob["comp.hybrid_power"] = powers[i, j] + prob["comp.throttle"] = throttles[i, j] + prob.set_val("comp.fltcond|h", 33000.0, units="ft") + prob["comp.fltcond|M"] = 0.8 prob.run_model() - pred[i,j,0] = prob['comp.thrust'][0].copy() - pred[i,j,1] = prob['comp.fuel_flow'][0].copy() - pred[i,j,2] = prob['comp.surge_margin'][0].copy() + pred[i, j, 0] = prob["comp.thrust"][0].copy() + pred[i, j, 1] = prob["comp.fuel_flow"][0].copy() + pred[i, j, 2] = prob["comp.surge_margin"][0].copy() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Hybrid Power (kW)') - plt.title('SFC (lb / hr lb) OM') + plt.xlabel("Throttle") + plt.ylabel("Hybrid Power (kW)") + plt.title("SFC (lb / hr lb) OM") # plt.contourf(machs, alts, pred[:,:,0]) - plt.contourf(throttles, powers, (pred[:,:,1] / pred[:,:,0])*60*60) + plt.contourf(throttles, powers, (pred[:, :, 1] / pred[:, :, 0]) * 60 * 60) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Hybrid Power (kW)') - plt.title('Fuel Flow (lb/s)') + plt.xlabel("Throttle") + plt.ylabel("Hybrid Power (kW)") + plt.title("Fuel Flow (lb/s)") # plt.contourf(throttles, powers, pred[:,:,0]) - plt.contourf(throttles, powers, pred[:,:,1], levels=20) + plt.contourf(throttles, powers, pred[:, :, 1], levels=20) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Hybrid Power (kW)') - plt.title('Thrust (lb)') + plt.xlabel("Throttle") + plt.ylabel("Hybrid Power (kW)") + plt.title("Thrust (lb)") # plt.contourf(throttles, powers, pred[:,:,0]) - plt.contourf(throttles, powers, pred[:,:,0], levels=20) + plt.contourf(throttles, powers, pred[:, :, 0], levels=20) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Hybrid Power (kW)') - plt.title('Surge margin') + plt.xlabel("Throttle") + plt.ylabel("Hybrid Power (kW)") + plt.title("Surge margin") # plt.contourf(throttles, powers, pred[:,:,0]) - plt.contourf(throttles, powers, pred[:,:,2], levels=20) + plt.contourf(throttles, powers, pred[:, :, 2], levels=20) plt.colorbar() plt.show() return comp + def N3(num_nodes=1, plot=False): """ Returns OpenMDAO component for thrust deck @@ -268,13 +301,13 @@ def N3(num_nodes=1, plot=False): num_nodes : int Number of analysis points to run (sets vec length; default 1) """ - file_root = openconcept.__path__[0] + r'/propulsion/empirical_data/n+3/' - thrustdata = np.load(file_root + r'/power_off/thrust.npy') - fuelburndata_0 = np.load(file_root + r'/power_off/wf.npy') - smwdata_0 = np.load(file_root + r'/power_off/SMW.npy') - altdata = np.load(file_root + r'/power_off/alt.npy') - machdata = np.load(file_root + r'/power_off/mach.npy') - throttledata = np.load(file_root + r'/power_off/throttle.npy') + file_root = openconcept.__path__[0] + r"/propulsion/empirical_data/n+3/" + thrustdata = np.load(file_root + r"/power_off/thrust.npy") + fuelburndata_0 = np.load(file_root + r"/power_off/wf.npy") + smwdata_0 = np.load(file_root + r"/power_off/SMW.npy") + altdata = np.load(file_root + r"/power_off/alt.npy") + machdata = np.load(file_root + r"/power_off/mach.npy") + throttledata = np.load(file_root + r"/power_off/throttle.npy") krigedata = [] for ialt in range(8): @@ -282,201 +315,221 @@ def N3(num_nodes=1, plot=False): for kthrot in range(11): fuelburnijk = fuelburndata_0[ialt, jmach, kthrot] if fuelburnijk > 0.0: - krigedata.append(np.array([throttledata[ialt, jmach, kthrot].copy(), - altdata[ialt, jmach, kthrot].copy(), - machdata[ialt, jmach ,kthrot].copy(), - thrustdata[ialt, jmach, kthrot].copy(), - fuelburnijk.copy(), - smwdata_0[ialt, jmach, kthrot].copy()])) + krigedata.append( + np.array( + [ + throttledata[ialt, jmach, kthrot].copy(), + altdata[ialt, jmach, kthrot].copy(), + machdata[ialt, jmach, kthrot].copy(), + thrustdata[ialt, jmach, kthrot].copy(), + fuelburnijk.copy(), + smwdata_0[ialt, jmach, kthrot].copy(), + ] + ) + ) a = np.array(krigedata) comp = om.MetaModelUnStructuredComp(vec_size=num_nodes) - comp.add_input('throttle', np.ones((num_nodes,))*1., training_data=a[:,0], units=None) - comp.add_input('fltcond|h', np.ones((num_nodes,))*0., training_data=a[:,1], units='ft') - comp.add_input('fltcond|M', np.ones((num_nodes,))*0.3, training_data=a[:,2], units=None) - - comp.add_output('thrust', np.ones((num_nodes,))*10000., - training_data=a[:,3], units='lbf', - surrogate=om.KrigingSurrogate(training_cache=file_root+r'n3_thrust_trained.zip')) - comp.add_output('fuel_flow', np.ones((num_nodes,))*3.0, - training_data=a[:,4], units='lbm/s', - surrogate=om.KrigingSurrogate(training_cache=file_root+r'n3_fuelflow_trained.zip')) - comp.add_output('surge_margin', np.ones((num_nodes,))*3.0, - training_data=a[:,5], units=None, - surrogate=om.KrigingSurrogate(training_cache=file_root+r'n3_smw_trained.zip')) - comp.options['default_surrogate'] = om.KrigingSurrogate(lapack_driver='gesvd') + comp.add_input("throttle", np.ones((num_nodes,)) * 1.0, training_data=a[:, 0], units=None) + comp.add_input("fltcond|h", np.ones((num_nodes,)) * 0.0, training_data=a[:, 1], units="ft") + comp.add_input("fltcond|M", np.ones((num_nodes,)) * 0.3, training_data=a[:, 2], units=None) + + comp.add_output( + "thrust", + np.ones((num_nodes,)) * 10000.0, + training_data=a[:, 3], + units="lbf", + surrogate=om.KrigingSurrogate(training_cache=file_root + r"n3_thrust_trained.zip"), + ) + comp.add_output( + "fuel_flow", + np.ones((num_nodes,)) * 3.0, + training_data=a[:, 4], + units="lbm/s", + surrogate=om.KrigingSurrogate(training_cache=file_root + r"n3_fuelflow_trained.zip"), + ) + comp.add_output( + "surge_margin", + np.ones((num_nodes,)) * 3.0, + training_data=a[:, 5], + units=None, + surrogate=om.KrigingSurrogate(training_cache=file_root + r"n3_smw_trained.zip"), + ) + comp.options["default_surrogate"] = om.KrigingSurrogate(lapack_driver="gesvd") if plot: import matplotlib.pyplot as plt + prob = om.Problem() - prob.model.add_subsystem('comp', comp) + prob.model.add_subsystem("comp", comp) prob.setup() - + nmachs = 7 nalts = 8 machs = np.linspace(0.2, 0.8, nmachs) - alts = np.linspace(0.0, 35000., nalts) - machs, alts = np.meshgrid(machs, alts, indexing='ij') + alts = np.linspace(0.0, 35000.0, nalts) + machs, alts = np.meshgrid(machs, alts, indexing="ij") pred = np.zeros((nmachs, nalts, 3)) for i in range(nmachs): for j in range(nalts): - prob['comp.throttle'] = 1.0 - prob['comp.fltcond|h'] = alts[i,j] - prob['comp.fltcond|M'] = machs[i,j] + prob["comp.throttle"] = 1.0 + prob["comp.fltcond|h"] = alts[i, j] + prob["comp.fltcond|M"] = machs[i, j] prob.run_model() - pred[i,j,0] = prob['comp.thrust'][0].copy() - pred[i,j,1] = prob['comp.fuel_flow'][0].copy() + pred[i, j, 0] = prob["comp.thrust"][0].copy() + pred[i, j, 1] = prob["comp.fuel_flow"][0].copy() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('SFC (lb / hr lb) OM') - plt.contourf(machs, alts, (pred[:,:,1] / pred[:,:,0])*60*60) + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("SFC (lb / hr lb) OM") + plt.contourf(machs, alts, (pred[:, :, 1] / pred[:, :, 0]) * 60 * 60) plt.colorbar() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('Fuel Flow (lb/s)') - plt.contourf(machs, alts, pred[:,:,1], levels=20) + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("Fuel Flow (lb/s)") + plt.contourf(machs, alts, pred[:, :, 1], levels=20) plt.colorbar() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('Thrust (lb)') - plt.contourf(machs, alts, pred[:,:,0], levels=20) + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("Thrust (lb)") + plt.contourf(machs, alts, pred[:, :, 0], levels=20) plt.colorbar() plt.show() nthrottles = 10 throttles = np.linspace(0.1, 1.0, nthrottles) - alts = np.linspace(0.0, 35000., nalts) - throttles, alts = np.meshgrid(throttles, alts, indexing='ij') + alts = np.linspace(0.0, 35000.0, nalts) + throttles, alts = np.meshgrid(throttles, alts, indexing="ij") pred = np.zeros((nthrottles, nalts, 3)) for i in range(nthrottles): for j in range(nalts): - prob['comp.throttle'] = throttles[i,j] - prob['comp.fltcond|h'] = alts[i,j] - prob['comp.fltcond|M'] = 0.5 + prob["comp.throttle"] = throttles[i, j] + prob["comp.fltcond|h"] = alts[i, j] + prob["comp.fltcond|M"] = 0.5 prob.run_model() - pred[i,j,0] = prob['comp.thrust'][0].copy() - pred[i,j,1] = prob['comp.fuel_flow'][0].copy() + pred[i, j, 0] = prob["comp.thrust"][0].copy() + pred[i, j, 1] = prob["comp.fuel_flow"][0].copy() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('SFC (lb / hr lb) OM') - plt.contourf(throttles, alts, (pred[:,:,1] / pred[:,:,0])*60*60) + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("SFC (lb / hr lb) OM") + plt.contourf(throttles, alts, (pred[:, :, 1] / pred[:, :, 0]) * 60 * 60) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('Fuel Flow (lb/s)') - plt.contourf(throttles, alts, pred[:,:,1], levels=20) + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("Fuel Flow (lb/s)") + plt.contourf(throttles, alts, pred[:, :, 1], levels=20) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('Thrust (lb)') - plt.contourf(throttles, alts, pred[:,:,0], levels=20) + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("Thrust (lb)") + plt.contourf(throttles, alts, pred[:, :, 0], levels=20) plt.colorbar() plt.show() return comp - def compare_thrust_decks(): import matplotlib.pyplot as plt + prob = om.Problem() from openconcept.propulsion import N3, N3Hybrid - prob.model.add_subsystem('n3', N3(num_nodes=1)) - prob.model.add_subsystem('n3hybrid', N3Hybrid(num_nodes=1)) - bal = prob.model.add_subsystem('bal', om.BalanceComp()) - bal.add_balance('n3hybrid_throttle', lower=0.05, upper=1.1, val=1.0) - prob.model.connect('n3.thrust', 'bal.rhs:n3hybrid_throttle') - prob.model.connect('n3hybrid.thrust', 'bal.lhs:n3hybrid_throttle') - prob.model.connect('bal.n3hybrid_throttle','n3hybrid.throttle') + prob.model.add_subsystem("n3", N3(num_nodes=1)) + prob.model.add_subsystem("n3hybrid", N3Hybrid(num_nodes=1)) + bal = prob.model.add_subsystem("bal", om.BalanceComp()) + bal.add_balance("n3hybrid_throttle", lower=0.05, upper=1.1, val=1.0) + prob.model.connect("n3.thrust", "bal.rhs:n3hybrid_throttle") + prob.model.connect("n3hybrid.thrust", "bal.lhs:n3hybrid_throttle") + prob.model.connect("bal.n3hybrid_throttle", "n3hybrid.throttle") prob.model.linear_solver = om.DirectSolver() - prob.model.nonlinear_solver = om.NewtonSolver(solve_subsystems=True) + prob.model.nonlinear_solver = om.NewtonSolver(solve_subsystems=True) prob.setup() - + nmachs = 7 nalts = 8 machs = np.linspace(0.2, 0.8, nmachs) - alts = np.linspace(0.0, 35000., nalts) - machs, alts = np.meshgrid(machs, alts, indexing='ij') + alts = np.linspace(0.0, 35000.0, nalts) + machs, alts = np.meshgrid(machs, alts, indexing="ij") predn3 = np.zeros((nmachs, nalts, 3)) predn3hybrid = np.zeros((nmachs, nalts, 3)) for i in range(nmachs): for j in range(nalts): - prob['n3.throttle'] = 1.0 - prob['n3.fltcond|h'] = alts[i,j] - prob['n3.fltcond|M'] = machs[i,j] - prob['n3hybrid.fltcond|h'] = alts[i,j] - prob['n3hybrid.fltcond|M'] = machs[i,j] + prob["n3.throttle"] = 1.0 + prob["n3.fltcond|h"] = alts[i, j] + prob["n3.fltcond|M"] = machs[i, j] + prob["n3hybrid.fltcond|h"] = alts[i, j] + prob["n3hybrid.fltcond|M"] = machs[i, j] prob.run_model() - predn3[i,j,0] = prob['n3.thrust'][0].copy() - predn3[i,j,1] = prob['n3.fuel_flow'][0].copy() - predn3hybrid[i,j,0] = prob['n3hybrid.thrust'][0].copy() - predn3hybrid[i,j,1] = prob['n3hybrid.fuel_flow'][0].copy() + predn3[i, j, 0] = prob["n3.thrust"][0].copy() + predn3[i, j, 1] = prob["n3.fuel_flow"][0].copy() + predn3hybrid[i, j, 0] = prob["n3hybrid.thrust"][0].copy() + predn3hybrid[i, j, 1] = prob["n3hybrid.fuel_flow"][0].copy() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('SFC (lb / hr lb) OM') - plt.contourf(machs, alts, (predn3[:,:,1] / predn3[:,:,0]) / (predn3hybrid[:,:,1] / predn3hybrid[:,:,0])) + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("SFC (lb / hr lb) OM") + plt.contourf(machs, alts, (predn3[:, :, 1] / predn3[:, :, 0]) / (predn3hybrid[:, :, 1] / predn3hybrid[:, :, 0])) plt.colorbar() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('Fuel Flow (lb/s)') - plt.contourf(machs, alts, predn3[:,:,1]/predn3hybrid[:,:,1], levels=20) + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("Fuel Flow (lb/s)") + plt.contourf(machs, alts, predn3[:, :, 1] / predn3hybrid[:, :, 1], levels=20) plt.colorbar() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('Thrust (lb)') - plt.contourf(machs, alts, predn3[:,:,0]/predn3hybrid[:,:,0], levels=20) + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("Thrust (lb)") + plt.contourf(machs, alts, predn3[:, :, 0] / predn3hybrid[:, :, 0], levels=20) plt.colorbar() plt.show() nthrottles = 10 throttles = np.linspace(0.1, 1.0, nthrottles) - alts = np.linspace(0.0, 35000., nalts) - throttles, alts = np.meshgrid(throttles, alts, indexing='ij') + alts = np.linspace(0.0, 35000.0, nalts) + throttles, alts = np.meshgrid(throttles, alts, indexing="ij") predn3 = np.zeros((nthrottles, nalts, 3)) predn3hybrid = np.zeros((nthrottles, nalts, 3)) for i in range(nthrottles): for j in range(nalts): - prob['n3.throttle'] = throttles[i,j] - prob['n3.fltcond|h'] = alts[i,j] - prob['n3.fltcond|M'] = 0.5 - prob['n3hybrid.fltcond|h'] = alts[i,j] - prob['n3hybrid.fltcond|M'] = 0.5 + prob["n3.throttle"] = throttles[i, j] + prob["n3.fltcond|h"] = alts[i, j] + prob["n3.fltcond|M"] = 0.5 + prob["n3hybrid.fltcond|h"] = alts[i, j] + prob["n3hybrid.fltcond|M"] = 0.5 prob.run_model() - predn3[i,j,0] = prob['n3.thrust'][0].copy() - predn3[i,j,1] = prob['n3.fuel_flow'][0].copy() - predn3hybrid[i,j,0] = prob['n3hybrid.thrust'][0].copy() - predn3hybrid[i,j,1] = prob['n3hybrid.fuel_flow'][0].copy() + predn3[i, j, 0] = prob["n3.thrust"][0].copy() + predn3[i, j, 1] = prob["n3.fuel_flow"][0].copy() + predn3hybrid[i, j, 0] = prob["n3hybrid.thrust"][0].copy() + predn3hybrid[i, j, 1] = prob["n3hybrid.fuel_flow"][0].copy() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('SFC (lb / hr lb) OM') - plt.contourf(throttles, alts, (predn3[:,:,1] / predn3[:,:,0]) / (predn3hybrid[:,:,1] / predn3hybrid[:,:,0])) + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("SFC (lb / hr lb) OM") + plt.contourf(throttles, alts, (predn3[:, :, 1] / predn3[:, :, 0]) / (predn3hybrid[:, :, 1] / predn3hybrid[:, :, 0])) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('Fuel Flow ratio (N3 / N3hybrid) at equal thrust, M=0.8') - plt.contourf(throttles, alts, predn3[:,:,1]/predn3hybrid[:,:,1], levels=10) + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("Fuel Flow ratio (N3 / N3hybrid) at equal thrust, M=0.8") + plt.contourf(throttles, alts, predn3[:, :, 1] / predn3hybrid[:, :, 1], levels=10) plt.colorbar() plt.figure() - plt.xlabel('Throttle') - plt.ylabel('Altitude') - plt.title('Thrust (lb)') - plt.contourf(throttles, alts, predn3[:,:,0]/predn3hybrid[:,:,0], levels=20) + plt.xlabel("Throttle") + plt.ylabel("Altitude") + plt.title("Thrust (lb)") + plt.contourf(throttles, alts, predn3[:, :, 0] / predn3hybrid[:, :, 0], levels=20) plt.colorbar() plt.show() + if __name__ == "__main__": - compare_thrust_decks() \ No newline at end of file + compare_thrust_decks() diff --git a/openconcept/propulsion/cfm56.py b/openconcept/propulsion/cfm56.py index f6f3ccb9..24a6c007 100644 --- a/openconcept/propulsion/cfm56.py +++ b/openconcept/propulsion/cfm56.py @@ -2,6 +2,7 @@ import openmdao.api as om import openconcept + def CFM56(num_nodes=1, plot=False): """ Returns OpenMDAO component for engine deck @@ -35,74 +36,98 @@ def CFM56(num_nodes=1, plot=False): num_nodes : int Number of analysis points to run (sets vec length; default 1) """ - file_root = openconcept.__path__[0] + r'/propulsion/empirical_data/cfm56/' - thrustdata = np.load(file_root + 'cfm56thrust.npy') - fuelburndata = np.load(file_root + 'cfm56wf.npy') - t4data = np.load(file_root + r'cfm56t4.npy') + file_root = openconcept.__path__[0] + r"/propulsion/empirical_data/cfm56/" + thrustdata = np.load(file_root + "cfm56thrust.npy") + fuelburndata = np.load(file_root + "cfm56wf.npy") + t4data = np.load(file_root + r"cfm56t4.npy") krigedata = [] - for ialt, altitude in enumerate(np.array([35, 30, 25, 20, 15, 10, 5, 0])*1000.): - for jmach, mach in enumerate(np.array([8, 7, 6, 5, 4, 3, 2, 1, 0.])*0.1): - for kthrot, throttle in enumerate(np.array([10, 9, 8, 7, 6, 5, 4, 3, 2])*0.1): + for ialt, altitude in enumerate(np.array([35, 30, 25, 20, 15, 10, 5, 0]) * 1000.0): + for jmach, mach in enumerate(np.array([8, 7, 6, 5, 4, 3, 2, 1, 0.0]) * 0.1): + for kthrot, throttle in enumerate(np.array([10, 9, 8, 7, 6, 5, 4, 3, 2]) * 0.1): thrustijk = thrustdata[ialt, jmach, kthrot] if thrustijk > 0.0: if not (mach > 0.5 and altitude == 0.0): - krigedata.append(np.array([throttle, altitude, mach, thrustijk.copy(), fuelburndata[ialt, jmach, kthrot].copy(), t4data[ialt, jmach, kthrot].copy()])) + krigedata.append( + np.array( + [ + throttle, + altitude, + mach, + thrustijk.copy(), + fuelburndata[ialt, jmach, kthrot].copy(), + t4data[ialt, jmach, kthrot].copy(), + ] + ) + ) a = np.array(krigedata) comp = om.MetaModelUnStructuredComp(vec_size=num_nodes) - comp.add_input('throttle', np.ones((num_nodes,))*1., training_data=a[:,0], units=None) - comp.add_input('fltcond|h', np.ones((num_nodes,))*0., training_data=a[:,1], units='ft') - comp.add_input('fltcond|M', np.ones((num_nodes,))*0.3, training_data=a[:,2], units=None) + comp.add_input("throttle", np.ones((num_nodes,)) * 1.0, training_data=a[:, 0], units=None) + comp.add_input("fltcond|h", np.ones((num_nodes,)) * 0.0, training_data=a[:, 1], units="ft") + comp.add_input("fltcond|M", np.ones((num_nodes,)) * 0.3, training_data=a[:, 2], units=None) - comp.add_output('thrust', np.ones((num_nodes,))*10000., - training_data=a[:,3], units='lbf', - surrogate=om.KrigingSurrogate(training_cache=file_root+'cfm56thrust_trained.zip')) - comp.add_output('fuel_flow', np.ones((num_nodes,))*3.0, - training_data=a[:,4], units='lbm/s', - surrogate=om.KrigingSurrogate(training_cache=file_root+'cfm56fuelburn_trained.zip')) - comp.add_output('T4', np.ones((num_nodes,))*3000., - training_data=a[:,5], units='degR', - surrogate=om.KrigingSurrogate(training_cache=file_root+'cfm56T4_trained.zip')) - comp.options['default_surrogate'] = om.KrigingSurrogate(lapack_driver='gesvd') + comp.add_output( + "thrust", + np.ones((num_nodes,)) * 10000.0, + training_data=a[:, 3], + units="lbf", + surrogate=om.KrigingSurrogate(training_cache=file_root + "cfm56thrust_trained.zip"), + ) + comp.add_output( + "fuel_flow", + np.ones((num_nodes,)) * 3.0, + training_data=a[:, 4], + units="lbm/s", + surrogate=om.KrigingSurrogate(training_cache=file_root + "cfm56fuelburn_trained.zip"), + ) + comp.add_output( + "T4", + np.ones((num_nodes,)) * 3000.0, + training_data=a[:, 5], + units="degR", + surrogate=om.KrigingSurrogate(training_cache=file_root + "cfm56T4_trained.zip"), + ) + comp.options["default_surrogate"] = om.KrigingSurrogate(lapack_driver="gesvd") if plot: import matplotlib.pyplot as plt + prob = om.Problem() - prob.model.add_subsystem('comp', comp) + prob.model.add_subsystem("comp", comp) prob.setup() machs = np.linspace(0.0, 0.8, 25) - alts = np.linspace(0.0, 35000., 25) + alts = np.linspace(0.0, 35000.0, 25) machs, alts = np.meshgrid(machs, alts) pred = np.zeros((25, 25, 3)) for i in range(25): for j in range(25): - prob['comp.throttle'] = 1.0 - prob['comp.fltcond|h'] = alts[i,j] - prob['comp.fltcond|M'] = machs[i,j] + prob["comp.throttle"] = 1.0 + prob["comp.fltcond|h"] = alts[i, j] + prob["comp.fltcond|M"] = machs[i, j] prob.run_model() - pred[i,j,0] = prob['comp.thrust'][0].copy() - pred[i,j,1] = prob['comp.fuel_flow'][0].copy() + pred[i, j, 0] = prob["comp.thrust"][0].copy() + pred[i, j, 1] = prob["comp.fuel_flow"][0].copy() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('SFC (lb / hr lb) OM') + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("SFC (lb / hr lb) OM") # plt.contourf(machs, alts, pred[:,:,0]) - plt.contourf(machs, alts, (pred[:,:,1] / pred[:,:,0])*60*60) + plt.contourf(machs, alts, (pred[:, :, 1] / pred[:, :, 0]) * 60 * 60) plt.colorbar() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('Thrust (lb)') + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("Thrust (lb)") # plt.contourf(machs, alts, pred[:,:,0]) - plt.contourf(machs, alts, pred[:,:,1], levels=20) + plt.contourf(machs, alts, pred[:, :, 1], levels=20) plt.colorbar() plt.figure() - plt.xlabel('Mach') - plt.ylabel('Altitude') - plt.title('Fuel Flow (lb)') + plt.xlabel("Mach") + plt.ylabel("Altitude") + plt.title("Fuel Flow (lb)") # plt.contourf(machs, alts, pred[:,:,0]) - plt.contourf(machs, alts, pred[:,:,0], levels=20) + plt.contourf(machs, alts, pred[:, :, 0], levels=20) plt.colorbar() plt.show() return comp diff --git a/openconcept/propulsion/empirical_data/prop_maps.py b/openconcept/propulsion/empirical_data/prop_maps.py index 7e400a40..e3b45c48 100644 --- a/openconcept/propulsion/empirical_data/prop_maps.py +++ b/openconcept/propulsion/empirical_data/prop_maps.py @@ -1,55 +1,65 @@ - import numpy as np from openmdao.api import Group, Problem, IndepVarComp, ExplicitComponent from openmdao.components.meta_model_structured_comp import MetaModelStructuredComp + def propeller_map_Raymer(vec_size=1): # Data from Raymer, Aircraft Design A Conceptual Approach, 4th Ed pg 498 fig 13.12 extrapolated in low cp range # For a 3 bladed constant-speed propeller - J = np.linspace(0.2,2.8,14) - cp = np.array([0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8]) - - #raymer_data = np.ones((9,14))*0.75 - raymer_data = np.array([[0.45,0.6,0.72,0.75,0.70,0.65,0.6,0.55,0.5,0.45,0.40,0.35,0.3,0.25], - [0.35,0.6,0.74,0.83,0.86,0.88,0.9,0.9,0.88,0.85,0.83,0.8,0.75,0.7], - [0.2,0.35,0.55,0.7,0.8,0.85,0.87,0.9,0.91,0.92,0.9,0.9,0.88,0.87], - [0.12,0.22,0.36,0.51,0.66,0.75,0.8,0.85,0.87,0.88,0.91,0.905,0.902,0.9], - [0.07,0.15,0.29,0.36,0.45,0.65,0.73,0.77,0.83,0.85,0.87,0.875,0.88,0.895], - [0.05,0.12,0.25,0.32,0.38,0.50,0.61,0.72,0.77,0.79,0.83,0.85,0.86,0.865], - [0.04,0.11,0.19,0.26,0.33,0.40,0.51,0.61,0.71,0.74,0.78,0.815,0.83,0.85], - [0.035,0.085,0.16,0.22,0.28,0.35,0.41,0.52,0.605,0.69,0.74,0.775,0.8,0.82], - [0.03,0.06,0.13,0.19,0.24,0.31,0.35,0.46,0.52,0.63,0.71,0.75,0.78,0.8]]) + J = np.linspace(0.2, 2.8, 14) + cp = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]) + + # raymer_data = np.ones((9,14))*0.75 + raymer_data = np.array( + [ + [0.45, 0.6, 0.72, 0.75, 0.70, 0.65, 0.6, 0.55, 0.5, 0.45, 0.40, 0.35, 0.3, 0.25], + [0.35, 0.6, 0.74, 0.83, 0.86, 0.88, 0.9, 0.9, 0.88, 0.85, 0.83, 0.8, 0.75, 0.7], + [0.2, 0.35, 0.55, 0.7, 0.8, 0.85, 0.87, 0.9, 0.91, 0.92, 0.9, 0.9, 0.88, 0.87], + [0.12, 0.22, 0.36, 0.51, 0.66, 0.75, 0.8, 0.85, 0.87, 0.88, 0.91, 0.905, 0.902, 0.9], + [0.07, 0.15, 0.29, 0.36, 0.45, 0.65, 0.73, 0.77, 0.83, 0.85, 0.87, 0.875, 0.88, 0.895], + [0.05, 0.12, 0.25, 0.32, 0.38, 0.50, 0.61, 0.72, 0.77, 0.79, 0.83, 0.85, 0.86, 0.865], + [0.04, 0.11, 0.19, 0.26, 0.33, 0.40, 0.51, 0.61, 0.71, 0.74, 0.78, 0.815, 0.83, 0.85], + [0.035, 0.085, 0.16, 0.22, 0.28, 0.35, 0.41, 0.52, 0.605, 0.69, 0.74, 0.775, 0.8, 0.82], + [0.03, 0.06, 0.13, 0.19, 0.24, 0.31, 0.35, 0.46, 0.52, 0.63, 0.71, 0.75, 0.78, 0.8], + ] + ) # Create regular grid interpolator instance - interp = MetaModelStructuredComp(method='scipy_cubic',extrapolate=True,vec_size=vec_size) - interp.add_input('cp', 0.3, cp) - interp.add_input('J', 1, J) - interp.add_output('eta_prop', 0.8, raymer_data) + interp = MetaModelStructuredComp(method="scipy_cubic", extrapolate=True, vec_size=vec_size) + interp.add_input("cp", 0.3, cp) + interp.add_input("J", 1, J) + interp.add_output("eta_prop", 0.8, raymer_data) return interp -def propeller_map_scaled(vec_size=1,design_J=2.2,design_cp=0.2): + +def propeller_map_scaled(vec_size=1, design_J=2.2, design_cp=0.2): # Data from Raymer, Aircraft Design A Conceptual Approach, 4th Ed pg 498 fig 13.12 extrapolated in low cp range # For a 3 bladed constant-speed propeller, scaled for higher design Cp - J = np.linspace(0.2,2.8*design_J/2.2,14) - cp = np.array([0,0.1,0.2,0.3,0.4,0.5])*design_cp/0.2 - - #raymer_data = np.ones((9,14))*0.75 - raymer_data = np.array([[0.45,0.6,0.72,0.75,0.70,0.65,0.6,0.55,0.5,0.45,0.40,0.35,0.3,0.25], - [0.35,0.6,0.74,0.83,0.86,0.88,0.9,0.9,0.88,0.85,0.83,0.8,0.75,0.7], - [0.2,0.35,0.55,0.7,0.8,0.85,0.87,0.9,0.91,0.92,0.9,0.9,0.88,0.87], - [0.12,0.22,0.36,0.51,0.66,0.75,0.8,0.85,0.87,0.88,0.91,0.905,0.902,0.9], - [0.07,0.15,0.29,0.36,0.45,0.65,0.73,0.77,0.83,0.85,0.87,0.875,0.88,0.895], - [0.05,0.12,0.25,0.32,0.38,0.50,0.61,0.72,0.77,0.79,0.83,0.85,0.86,0.865]]) + J = np.linspace(0.2, 2.8 * design_J / 2.2, 14) + cp = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5]) * design_cp / 0.2 + + # raymer_data = np.ones((9,14))*0.75 + raymer_data = np.array( + [ + [0.45, 0.6, 0.72, 0.75, 0.70, 0.65, 0.6, 0.55, 0.5, 0.45, 0.40, 0.35, 0.3, 0.25], + [0.35, 0.6, 0.74, 0.83, 0.86, 0.88, 0.9, 0.9, 0.88, 0.85, 0.83, 0.8, 0.75, 0.7], + [0.2, 0.35, 0.55, 0.7, 0.8, 0.85, 0.87, 0.9, 0.91, 0.92, 0.9, 0.9, 0.88, 0.87], + [0.12, 0.22, 0.36, 0.51, 0.66, 0.75, 0.8, 0.85, 0.87, 0.88, 0.91, 0.905, 0.902, 0.9], + [0.07, 0.15, 0.29, 0.36, 0.45, 0.65, 0.73, 0.77, 0.83, 0.85, 0.87, 0.875, 0.88, 0.895], + [0.05, 0.12, 0.25, 0.32, 0.38, 0.50, 0.61, 0.72, 0.77, 0.79, 0.83, 0.85, 0.86, 0.865], + ] + ) # Create regular grid interpolator instance - interp = MetaModelStructuredComp(method='scipy_cubic',extrapolate=True,vec_size=vec_size) - interp.add_input('cp', 0.3, cp) - interp.add_input('J', 1, J) - interp.add_output('eta_prop', 0.8, raymer_data) + interp = MetaModelStructuredComp(method="scipy_cubic", extrapolate=True, vec_size=vec_size) + interp.add_input("cp", 0.3, cp) + interp.add_input("J", 1, J) + interp.add_output("eta_prop", 0.8, raymer_data) return interp + def propeller_map_highpower(vec_size=1): # Data from https://frautech.wordpress.com/2011/01/28/design-fridays-thats-a-big-prop/ - J = np.linspace(0.0,4.0,9) - cp = np.linspace(0.0,2.5,13) + J = np.linspace(0.0, 4.0, 9) + cp = np.linspace(0.0, 2.5, 13) # data = np.array([[0.28,0.51,0.65,0.66,0.65,0.64,0.63,0.62,0.61], # [0.27,0.50,0.71,0.82,0.81,0.70,0.68,0.67,0.66], @@ -60,62 +70,142 @@ def propeller_map_highpower(vec_size=1): # [0.22,0.38,0.61,0.78,0.85,0.88,0.91,0.90,0.86], # [0.21,0.34,0.58,0.73,0.83,0.876,0.904,0.91,0.88], # [0.20,0.31,0.53,0.71,0.81,0.87,0.895,0.91,0.882]]) - data = np.array([[0.28,0.51,0.65,0.66,0.65,0.64,0.63,0.62,0.61], - [0.20,0.50,0.71,0.82,0.81,0.70,0.68,0.67,0.66], - [0.19,0.49,0.72,0.83,0.86,0.85,0.75,0.70,0.69], - [0.18,0.45,0.71,0.82,0.865,0.875,0.84,0.79,0.72], - [0.17,0.42,0.69,0.815,0.87,0.885,0.878,0.84,0.80], - [0.155,0.40,0.65,0.81,0.865,0.89,0.903,0.873,0.83], - [0.13,0.38,0.61,0.78,0.85,0.88,0.91,0.90,0.86], - [0.12,0.34,0.58,0.73,0.83,0.876,0.904,0.91,0.88], - [0.10,0.31,0.53,0.71,0.81,0.87,0.895,0.91,0.882], - [0.08,0.25,0.44,0.62,0.75,0.84,0.88,0.89,0.87], - [0.06,0.18,0.35,0.50,0.68,0.79,0.86,0.86,0.85], - [0.05,0.14,0.25,0.40,0.55,0.70,0.79,0.80,0.72], - [0.04,0.12,0.19,0.29,0.40,0.50,0.60,0.60,0.50]]) - - data[:,0] = np.zeros(13) + data = np.array( + [ + [0.28, 0.51, 0.65, 0.66, 0.65, 0.64, 0.63, 0.62, 0.61], + [0.20, 0.50, 0.71, 0.82, 0.81, 0.70, 0.68, 0.67, 0.66], + [0.19, 0.49, 0.72, 0.83, 0.86, 0.85, 0.75, 0.70, 0.69], + [0.18, 0.45, 0.71, 0.82, 0.865, 0.875, 0.84, 0.79, 0.72], + [0.17, 0.42, 0.69, 0.815, 0.87, 0.885, 0.878, 0.84, 0.80], + [0.155, 0.40, 0.65, 0.81, 0.865, 0.89, 0.903, 0.873, 0.83], + [0.13, 0.38, 0.61, 0.78, 0.85, 0.88, 0.91, 0.90, 0.86], + [0.12, 0.34, 0.58, 0.73, 0.83, 0.876, 0.904, 0.91, 0.88], + [0.10, 0.31, 0.53, 0.71, 0.81, 0.87, 0.895, 0.91, 0.882], + [0.08, 0.25, 0.44, 0.62, 0.75, 0.84, 0.88, 0.89, 0.87], + [0.06, 0.18, 0.35, 0.50, 0.68, 0.79, 0.86, 0.86, 0.85], + [0.05, 0.14, 0.25, 0.40, 0.55, 0.70, 0.79, 0.80, 0.72], + [0.04, 0.12, 0.19, 0.29, 0.40, 0.50, 0.60, 0.60, 0.50], + ] + ) + + data[:, 0] = np.zeros(13) # Create regular grid interpolator instance - interp = MetaModelStructuredComp(method='scipy_cubic',extrapolate=True,vec_size=vec_size) - interp.add_input('cp', 0.3, cp) - interp.add_input('J', 1, J) - interp.add_output('eta_prop', 0.8, data) + interp = MetaModelStructuredComp(method="scipy_cubic", extrapolate=True, vec_size=vec_size) + interp.add_input("cp", 0.3, cp) + interp.add_input("J", 1, J) + interp.add_output("eta_prop", 0.8, data) return interp + class ConstantPropEfficiency(ExplicitComponent): def initialize(self): - #define technology factors - self.options.declare('vec_size', default=1, desc='Number of flight/control conditions') + # define technology factors + self.options.declare("vec_size", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['vec_size'] - self.add_input('cp', desc='Power coefficient',shape=(nn,)) - self.add_input('J', desc='Advance ratio', shape=(nn,)) - self.add_output('eta_prop', desc='Propulsive efficiency',shape=(nn,)) - self.declare_partials(['eta_prop'], ['*'], rows=range(nn), cols=range(nn), val=np.zeros(nn)) + nn = self.options["vec_size"] + self.add_input("cp", desc="Power coefficient", shape=(nn,)) + self.add_input("J", desc="Advance ratio", shape=(nn,)) + self.add_output("eta_prop", desc="Propulsive efficiency", shape=(nn,)) + self.declare_partials(["eta_prop"], ["*"], rows=range(nn), cols=range(nn), val=np.zeros(nn)) def compute(self, inputs, outputs): - outputs['eta_prop'] = 0.85 + outputs["eta_prop"] = 0.85 + def propeller_map_constant_prop_efficiency(vec_size=1): interp = ConstantPropEfficiency(vec_size=vec_size) return interp + def static_propeller_map_Raymer(vec_size=1): - #Data from Raymer for static thrust of 3-bladed propeller - cp = np.linspace(0.0,0.60,25) - raymer_static_data = np.array([2.5,3.0,2.55,2.0,1.85,1.5,1.25,1.05,0.95,0.86,0.79,0.70,0.62,0.53,0.45,0.38,0.32,0.28,0.24,0.21,0.18,0.16,0.14,0.12,0.10]) - interp = MetaModelStructuredComp(method='scipy_cubic',extrapolate=True,vec_size=vec_size) - interp.add_input('cp',0.15,cp) - interp.add_output('ct_over_cp',1.5,raymer_static_data) + # Data from Raymer for static thrust of 3-bladed propeller + cp = np.linspace(0.0, 0.60, 25) + raymer_static_data = np.array( + [ + 2.5, + 3.0, + 2.55, + 2.0, + 1.85, + 1.5, + 1.25, + 1.05, + 0.95, + 0.86, + 0.79, + 0.70, + 0.62, + 0.53, + 0.45, + 0.38, + 0.32, + 0.28, + 0.24, + 0.21, + 0.18, + 0.16, + 0.14, + 0.12, + 0.10, + ] + ) + interp = MetaModelStructuredComp(method="scipy_cubic", extrapolate=True, vec_size=vec_size) + interp.add_input("cp", 0.15, cp) + interp.add_output("ct_over_cp", 1.5, raymer_static_data) return interp + def static_propeller_map_highpower(vec_size=1): - #Factoring up the thrust of the Raymer static thrust data to match the high power data - cp = np.linspace(0.0,1.0,41) - factored_raymer_static_data = np.array([2.5,3.0,2.55,2.0,1.85,1.5,1.25,1.05,0.95,0.86,0.79,0.70,0.62,0.53,0.45,0.38,0.32,0.28,0.24,0.21,0.18,0.16,0.14,0.12,0.10,0.09,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08]) - factored_raymer_static_data[6:] = factored_raymer_static_data[6:]*1.2 - interp = MetaModelStructuredComp(method='scipy_cubic',extrapolate=True,vec_size=vec_size) - interp.add_input('cp',0.15,cp) - interp.add_output('ct_over_cp',1.5,factored_raymer_static_data) + # Factoring up the thrust of the Raymer static thrust data to match the high power data + cp = np.linspace(0.0, 1.0, 41) + factored_raymer_static_data = np.array( + [ + 2.5, + 3.0, + 2.55, + 2.0, + 1.85, + 1.5, + 1.25, + 1.05, + 0.95, + 0.86, + 0.79, + 0.70, + 0.62, + 0.53, + 0.45, + 0.38, + 0.32, + 0.28, + 0.24, + 0.21, + 0.18, + 0.16, + 0.14, + 0.12, + 0.10, + 0.09, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + ] + ) + factored_raymer_static_data[6:] = factored_raymer_static_data[6:] * 1.2 + interp = MetaModelStructuredComp(method="scipy_cubic", extrapolate=True, vec_size=vec_size) + interp.add_input("cp", 0.15, cp) + interp.add_output("ct_over_cp", 1.5, factored_raymer_static_data) return interp diff --git a/openconcept/propulsion/generator.py b/openconcept/propulsion/generator.py index bdb9f2bd..49b4e5f7 100644 --- a/openconcept/propulsion/generator.py +++ b/openconcept/propulsion/generator.py @@ -41,60 +41,62 @@ class SimpleGenerator(ExplicitComponent): cost_base : float Base cost (default 1 USD) B """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") # define technology factors - self.options.declare('efficiency', default=1., desc='Efficiency (dimensionless)') - self.options.declare('weight_inc', default=1 / 5000, desc='kg/W') - self.options.declare('weight_base', default=0., desc='kg base weight') - self.options.declare('cost_inc', default=100.0 / 745.0, desc='$ cost per watt') - self.options.declare('cost_base', default=1., desc='$ cost base') + self.options.declare("efficiency", default=1.0, desc="Efficiency (dimensionless)") + self.options.declare("weight_inc", default=1 / 5000, desc="kg/W") + self.options.declare("weight_base", default=0.0, desc="kg base weight") + self.options.declare("cost_inc", default=100.0 / 745.0, desc="$ cost per watt") + self.options.declare("cost_base", default=1.0, desc="$ cost base") def setup(self): - nn = self.options['num_nodes'] - self.add_input('shaft_power_in', units='W', desc='Input shaft power', shape=(nn,)) - self.add_input('elec_power_rating', units='W', desc='Rated output power') + nn = self.options["num_nodes"] + self.add_input("shaft_power_in", units="W", desc="Input shaft power", shape=(nn,)) + self.add_input("elec_power_rating", units="W", desc="Rated output power") # outputs and partials - eta_g = self.options['efficiency'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] + eta_g = self.options["efficiency"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] - self.add_output('elec_power_out', units='W', desc='Output electric power', shape=(nn,)) - self.add_output('heat_out', units='W', desc='Waste heat out', shape=(nn,)) - self.add_output('component_cost', units='USD', desc='Generator component cost') - self.add_output('component_weight', units='kg', desc='Generator component weight') - self.add_output('component_sizing_margin', desc='Fraction of rated power', shape=(nn,)) + self.add_output("elec_power_out", units="W", desc="Output electric power", shape=(nn,)) + self.add_output("heat_out", units="W", desc="Waste heat out", shape=(nn,)) + self.add_output("component_cost", units="USD", desc="Generator component cost") + self.add_output("component_weight", units="kg", desc="Generator component weight") + self.add_output("component_sizing_margin", desc="Fraction of rated power", shape=(nn,)) - self.declare_partials('elec_power_out', 'shaft_power_in', - val=eta_g * np.ones(nn), rows=range(nn), cols=range(nn)) - self.declare_partials('heat_out', 'shaft_power_in', - val=(1 - eta_g) * np.ones(nn), rows=range(nn), cols=range(nn)) - self.declare_partials('component_cost', 'elec_power_rating', val=cost_inc) - self.declare_partials('component_weight', 'elec_power_rating', val=weight_inc) - self.declare_partials('component_sizing_margin', 'shaft_power_in', - rows=range(nn), cols=range(nn)) - self.declare_partials('component_sizing_margin', 'elec_power_rating') + self.declare_partials( + "elec_power_out", "shaft_power_in", val=eta_g * np.ones(nn), rows=range(nn), cols=range(nn) + ) + self.declare_partials( + "heat_out", "shaft_power_in", val=(1 - eta_g) * np.ones(nn), rows=range(nn), cols=range(nn) + ) + self.declare_partials("component_cost", "elec_power_rating", val=cost_inc) + self.declare_partials("component_weight", "elec_power_rating", val=weight_inc) + self.declare_partials("component_sizing_margin", "shaft_power_in", rows=range(nn), cols=range(nn)) + self.declare_partials("component_sizing_margin", "elec_power_rating") def compute(self, inputs, outputs): - eta_g = self.options['efficiency'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] + eta_g = self.options["efficiency"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] - outputs['elec_power_out'] = inputs['shaft_power_in'] * eta_g - outputs['heat_out'] = inputs['shaft_power_in'] * (1 - eta_g) - outputs['component_cost'] = inputs['elec_power_rating'] * cost_inc + cost_base - outputs['component_weight'] = inputs['elec_power_rating'] * weight_inc + weight_base - outputs['component_sizing_margin'] = (inputs['shaft_power_in'] * - eta_g / inputs['elec_power_rating']) + outputs["elec_power_out"] = inputs["shaft_power_in"] * eta_g + outputs["heat_out"] = inputs["shaft_power_in"] * (1 - eta_g) + outputs["component_cost"] = inputs["elec_power_rating"] * cost_inc + cost_base + outputs["component_weight"] = inputs["elec_power_rating"] * weight_inc + weight_base + outputs["component_sizing_margin"] = inputs["shaft_power_in"] * eta_g / inputs["elec_power_rating"] def compute_partials(self, inputs, J): - eta_g = self.options['efficiency'] - J['component_sizing_margin', 'shaft_power_in'] = eta_g / inputs['elec_power_rating'] - J['component_sizing_margin', 'elec_power_rating'] = - (eta_g * inputs['shaft_power_in'] / - inputs['elec_power_rating'] ** 2) + eta_g = self.options["efficiency"] + J["component_sizing_margin", "shaft_power_in"] = eta_g / inputs["elec_power_rating"] + J["component_sizing_margin", "elec_power_rating"] = -( + eta_g * inputs["shaft_power_in"] / inputs["elec_power_rating"] ** 2 + ) diff --git a/openconcept/propulsion/motor.py b/openconcept/propulsion/motor.py index dd7ea292..2ffd02ac 100644 --- a/openconcept/propulsion/motor.py +++ b/openconcept/propulsion/motor.py @@ -47,62 +47,61 @@ class SimpleMotor(ExplicitComponent): def initialize(self): # define technology factors - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('efficiency', default=1., desc='Efficiency (dimensionless)') - self.options.declare('weight_inc', default=1 / 5000, desc='kg/W') # 5kW/kg - self.options.declare('weight_base', default=0., desc='kg base weight') - self.options.declare('cost_inc', default=100 / 745, desc='$ cost per watt') - self.options.declare('cost_base', default=1., desc='$ cost base') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("efficiency", default=1.0, desc="Efficiency (dimensionless)") + self.options.declare("weight_inc", default=1 / 5000, desc="kg/W") # 5kW/kg + self.options.declare("weight_base", default=0.0, desc="kg base weight") + self.options.declare("cost_inc", default=100 / 745, desc="$ cost per watt") + self.options.declare("cost_base", default=1.0, desc="$ cost base") def setup(self): - nn = self.options['num_nodes'] - self.add_input('throttle', desc='Throttle input (Fractional)', shape=(nn,)) - self.add_input('elec_power_rating', units='W', desc='Rated electrical power (load)') + nn = self.options["num_nodes"] + self.add_input("throttle", desc="Throttle input (Fractional)", shape=(nn,)) + self.add_input("elec_power_rating", units="W", desc="Rated electrical power (load)") # outputs and partials - eta_m = self.options['efficiency'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] + eta_m = self.options["efficiency"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] - self.add_output('shaft_power_out', units='W', desc='Output shaft power', shape=(nn,)) - self.add_output('heat_out', units='W', desc='Waste heat out', shape=(nn,)) - self.add_output('elec_load', units='W', desc='Electrical load consumed', shape=(nn,)) - self.add_output('component_cost', units='USD', desc='Motor component cost') - self.add_output('component_weight', units='kg', desc='Motor component weight') - self.add_output('component_sizing_margin', desc='Fraction of rated power', shape=(nn,)) - self.declare_partials('shaft_power_out', 'elec_power_rating') - self.declare_partials('shaft_power_out', 'throttle', - rows=range(nn), cols=range(nn)) - self.declare_partials('heat_out', 'elec_power_rating') - self.declare_partials('heat_out', 'throttle', 'elec_power_rating', - rows=range(nn), cols=range(nn)) - self.declare_partials('elec_load', 'elec_power_rating') - self.declare_partials('elec_load', 'throttle', rows=range(nn), cols=range(nn)) - self.declare_partials('component_cost', 'elec_power_rating', val=cost_inc) - self.declare_partials('component_weight', 'elec_power_rating', val=weight_inc) - self.declare_partials('component_sizing_margin', 'throttle', - val=1.0 * np.ones(nn), rows=range(nn), cols=range(nn)) + self.add_output("shaft_power_out", units="W", desc="Output shaft power", shape=(nn,)) + self.add_output("heat_out", units="W", desc="Waste heat out", shape=(nn,)) + self.add_output("elec_load", units="W", desc="Electrical load consumed", shape=(nn,)) + self.add_output("component_cost", units="USD", desc="Motor component cost") + self.add_output("component_weight", units="kg", desc="Motor component weight") + self.add_output("component_sizing_margin", desc="Fraction of rated power", shape=(nn,)) + self.declare_partials("shaft_power_out", "elec_power_rating") + self.declare_partials("shaft_power_out", "throttle", rows=range(nn), cols=range(nn)) + self.declare_partials("heat_out", "elec_power_rating") + self.declare_partials("heat_out", "throttle", "elec_power_rating", rows=range(nn), cols=range(nn)) + self.declare_partials("elec_load", "elec_power_rating") + self.declare_partials("elec_load", "throttle", rows=range(nn), cols=range(nn)) + self.declare_partials("component_cost", "elec_power_rating", val=cost_inc) + self.declare_partials("component_weight", "elec_power_rating", val=weight_inc) + self.declare_partials( + "component_sizing_margin", "throttle", val=1.0 * np.ones(nn), rows=range(nn), cols=range(nn) + ) def compute(self, inputs, outputs): - eta_m = self.options['efficiency'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] - outputs['shaft_power_out'] = inputs['throttle'] * inputs['elec_power_rating'] * eta_m - outputs['heat_out'] = inputs['throttle'] * inputs['elec_power_rating'] * (1 - eta_m) - outputs['elec_load'] = inputs['throttle'] * inputs['elec_power_rating'] - outputs['component_cost'] = inputs['elec_power_rating'] * cost_inc + cost_base - outputs['component_weight'] = inputs['elec_power_rating'] * weight_inc + weight_base - outputs['component_sizing_margin'] = inputs['throttle'] + eta_m = self.options["efficiency"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] + outputs["shaft_power_out"] = inputs["throttle"] * inputs["elec_power_rating"] * eta_m + outputs["heat_out"] = inputs["throttle"] * inputs["elec_power_rating"] * (1 - eta_m) + outputs["elec_load"] = inputs["throttle"] * inputs["elec_power_rating"] + outputs["component_cost"] = inputs["elec_power_rating"] * cost_inc + cost_base + outputs["component_weight"] = inputs["elec_power_rating"] * weight_inc + weight_base + outputs["component_sizing_margin"] = inputs["throttle"] def compute_partials(self, inputs, J): - eta_m = self.options['efficiency'] - J['shaft_power_out', 'throttle'] = inputs['elec_power_rating'] * eta_m - J['shaft_power_out', 'elec_power_rating'] = inputs['throttle'] * eta_m - J['heat_out', 'throttle'] = inputs['elec_power_rating'] * (1 - eta_m) - J['heat_out', 'elec_power_rating'] = inputs['throttle'] * (1 - eta_m) - J['elec_load', 'throttle'] = inputs['elec_power_rating'] - J['elec_load', 'elec_power_rating'] = inputs['throttle'] + eta_m = self.options["efficiency"] + J["shaft_power_out", "throttle"] = inputs["elec_power_rating"] * eta_m + J["shaft_power_out", "elec_power_rating"] = inputs["throttle"] * eta_m + J["heat_out", "throttle"] = inputs["elec_power_rating"] * (1 - eta_m) + J["heat_out", "elec_power_rating"] = inputs["throttle"] * (1 - eta_m) + J["elec_load", "throttle"] = inputs["elec_power_rating"] + J["elec_load", "elec_power_rating"] = inputs["throttle"] diff --git a/openconcept/propulsion/propeller.py b/openconcept/propulsion/propeller.py index dfedb25e..6c098395 100644 --- a/openconcept/propulsion/propeller.py +++ b/openconcept/propulsion/propeller.py @@ -1,7 +1,15 @@ import numpy as np from openmdao.api import ExplicitComponent from openmdao.api import Group -from .empirical_data.prop_maps import propeller_map_Raymer, propeller_map_highpower, static_propeller_map_Raymer, static_propeller_map_highpower, propeller_map_scaled, propeller_map_constant_prop_efficiency +from .empirical_data.prop_maps import ( + propeller_map_Raymer, + propeller_map_highpower, + static_propeller_map_Raymer, + static_propeller_map_highpower, + propeller_map_scaled, + propeller_map_constant_prop_efficiency, +) + class SimplePropeller(Group): """This propeller is representative of a constant-speed prop. @@ -42,16 +50,18 @@ class SimplePropeller(Group): design_J : float Design advance ratio (J) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('num_blades', default=4, desc='Number of prop blades') - self.options.declare('design_cp',default=0.2,desc='Design cruise power coefficient cp') - self.options.declare('design_J',default=2.2,desc='Design advance ratio J=V/n/D') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("num_blades", default=4, desc="Number of prop blades") + self.options.declare("design_cp", default=0.2, desc="Design cruise power coefficient cp") + self.options.declare("design_J", default=2.2, desc="Design advance ratio J=V/n/D") + def setup(self): - nn = self.options['num_nodes'] - n_blades = self.options['num_blades'] - design_J = self.options['design_J'] - design_cp = self.options['design_cp'] + nn = self.options["num_nodes"] + n_blades = self.options["num_blades"] + design_J = self.options["design_J"] + design_cp = self.options["design_cp"] if n_blades == 3: propmap = propeller_map_Raymer(nn) staticpropmap = static_propeller_map_Raymer(nn) @@ -59,115 +69,129 @@ def setup(self): propmap = propeller_map_highpower(nn) staticpropmap = static_propeller_map_highpower(nn) else: - raise NotImplementedError('You will need to define a propeller map valid for this number of blades') - self.add_subsystem('propcalcs',PropCoefficients(num_nodes=nn),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('propmap',propmap,promotes_outputs=["*"]) - self.add_subsystem('staticpropmap',staticpropmap,promotes_outputs=["*"]) - self.add_subsystem('thrustcalc',ThrustCalc(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) - self.add_subsystem('propweights',WeightCalc(num_blades=n_blades), promotes_inputs=["*"], promotes_outputs=["*"]) - self.connect('cp','propmap.cp') - self.connect('cp','staticpropmap.cp') - self.connect('J','propmap.J') + raise NotImplementedError("You will need to define a propeller map valid for this number of blades") + self.add_subsystem("propcalcs", PropCoefficients(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("propmap", propmap, promotes_outputs=["*"]) + self.add_subsystem("staticpropmap", staticpropmap, promotes_outputs=["*"]) + self.add_subsystem("thrustcalc", ThrustCalc(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "propweights", WeightCalc(num_blades=n_blades), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.connect("cp", "propmap.cp") + self.connect("cp", "staticpropmap.cp") + self.connect("J", "propmap.J") class WeightCalc(ExplicitComponent): def initialize(self): - self.options.declare('num_blades', default=4, desc='Number of prop blades') + self.options.declare("num_blades", default=4, desc="Number of prop blades") def setup(self): - self.add_input('power_rating', units='hp', desc='Propulsor power rating') - self.add_input('diameter', units='ft', desc='Prop diameter in feet') - self.add_output('component_weight', units='lb', desc='Propeller weight') - self.declare_partials('component_weight',['power_rating','diameter']) + self.add_input("power_rating", units="hp", desc="Propulsor power rating") + self.add_input("diameter", units="ft", desc="Prop diameter in feet") + self.add_output("component_weight", units="lb", desc="Propeller weight") + self.declare_partials("component_weight", ["power_rating", "diameter"]) def compute(self, inputs, outputs): - #Method from Roskam SVC6p90eq6.14 - Kprop2 = 0.108 #for turboprops - n_blades = self.options['num_blades'] - W_prop = Kprop2 * (inputs['diameter']*inputs['power_rating']*n_blades**0.5)**0.782 - outputs['component_weight'] = W_prop + # Method from Roskam SVC6p90eq6.14 + Kprop2 = 0.108 # for turboprops + n_blades = self.options["num_blades"] + W_prop = Kprop2 * (inputs["diameter"] * inputs["power_rating"] * n_blades**0.5) ** 0.782 + outputs["component_weight"] = W_prop def compute_partials(self, inputs, J): - Kprop2 = 0.108 #for turboprops - n_blades = self.options['num_blades'] - J['component_weight','power_rating'] = 0.782 * Kprop2 * (inputs['diameter']*inputs['power_rating']*n_blades**0.5)**(0.782-1) * (inputs['diameter']*n_blades**0.5) - J['component_weight','diameter'] = 0.782 * Kprop2 * (inputs['diameter']*inputs['power_rating']*n_blades**0.5)**(0.782-1) * (inputs['power_rating']*n_blades**0.5) + Kprop2 = 0.108 # for turboprops + n_blades = self.options["num_blades"] + J["component_weight", "power_rating"] = ( + 0.782 + * Kprop2 + * (inputs["diameter"] * inputs["power_rating"] * n_blades**0.5) ** (0.782 - 1) + * (inputs["diameter"] * n_blades**0.5) + ) + J["component_weight", "diameter"] = ( + 0.782 + * Kprop2 + * (inputs["diameter"] * inputs["power_rating"] * n_blades**0.5) ** (0.782 - 1) + * (inputs["power_rating"] * n_blades**0.5) + ) class ThrustCalc(ExplicitComponent): def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('cp', desc='power coefficient',shape=(nn,)) - self.add_input('eta_prop', desc='propulsive efficiency factor',shape=(nn,)) - self.add_input('J', desc = 'advance ratio',shape=(nn,)) - self.add_input('fltcond|rho', units='kg / m ** 3', desc='Air density',shape=(nn,)) - self.add_input('rpm', units='rpm', val=2500*np.ones(nn), desc='Prop speed in rpm') - self.add_input('diameter', units='m', val=2.5, desc='Prop diameter in m') - self.add_input('ct_over_cp', val=1.5*np.ones(nn), desc='ct/cp from raymer for static condition') - - self.add_output('thrust', desc='Propeller thrust', units='N',shape=(nn,)) - - self.declare_partials('thrust',['cp','eta_prop','J','fltcond|rho','rpm','ct_over_cp'], rows=range(nn), cols=range(nn)) - self.declare_partials('thrust','diameter') + nn = self.options["num_nodes"] + self.add_input("cp", desc="power coefficient", shape=(nn,)) + self.add_input("eta_prop", desc="propulsive efficiency factor", shape=(nn,)) + self.add_input("J", desc="advance ratio", shape=(nn,)) + self.add_input("fltcond|rho", units="kg / m ** 3", desc="Air density", shape=(nn,)) + self.add_input("rpm", units="rpm", val=2500 * np.ones(nn), desc="Prop speed in rpm") + self.add_input("diameter", units="m", val=2.5, desc="Prop diameter in m") + self.add_input("ct_over_cp", val=1.5 * np.ones(nn), desc="ct/cp from raymer for static condition") + + self.add_output("thrust", desc="Propeller thrust", units="N", shape=(nn,)) + + self.declare_partials( + "thrust", ["cp", "eta_prop", "J", "fltcond|rho", "rpm", "ct_over_cp"], rows=range(nn), cols=range(nn) + ) + self.declare_partials("thrust", "diameter") def compute(self, inputs, outputs): - #for advance ratio j between 0.10 and 0.20, linearly interpolate the thrust coefficient from the two surrogate models + # for advance ratio j between 0.10 and 0.20, linearly interpolate the thrust coefficient from the two surrogate models jinterp_min = 0.10 jinterp_max = 0.20 - j = inputs['J'] - #print(inputs['eta_prop']) - static_idx = np.where(j<=jinterp_min) - dynamic_idx = np.where(j>=jinterp_max) - tmp = np.logical_and(j>jinterp_min, j= jinterp_max) + tmp = np.logical_and(j > jinterp_min, j < jinterp_max) interp_idx = np.where(tmp) - cp = inputs['cp'] - nn = self.options['num_nodes'] + cp = inputs["cp"] + nn = self.options["num_nodes"] ct = np.zeros(nn) - ct1 = inputs['ct_over_cp'] * cp - ct2 = cp * inputs['eta_prop'] / j - #if j <= jinterp_min: + ct1 = inputs["ct_over_cp"] * cp + ct2 = cp * inputs["eta_prop"] / j + # if j <= jinterp_min: ct[static_idx] = ct1[static_idx] - #if j > jinterp_min and < jinterp_max: - interv = np.ones(nn)*jinterp_max - np.ones(nn)*jinterp_min - interp1 = (np.ones(nn)*jinterp_max - j) / interv - interp2 = (j - np.ones(nn)*jinterp_min) / interv + # if j > jinterp_min and < jinterp_max: + interv = np.ones(nn) * jinterp_max - np.ones(nn) * jinterp_min + interp1 = (np.ones(nn) * jinterp_max - j) / interv + interp2 = (j - np.ones(nn) * jinterp_min) / interv ct[interp_idx] = interp1[interp_idx] * ct1[interp_idx] + interp2[interp_idx] * ct2[interp_idx] - #else if j >= jinterp_max + # else if j >= jinterp_max ct[dynamic_idx] = ct2[dynamic_idx] - outputs['thrust'] = ct * inputs['fltcond|rho'] * (inputs['rpm']/60.)**2 * inputs['diameter']**4 + outputs["thrust"] = ct * inputs["fltcond|rho"] * (inputs["rpm"] / 60.0) ** 2 * inputs["diameter"] ** 4 def compute_partials(self, inputs, J): - #for advance ratio j between 0.10 and 0.20, linearly interpolate between the two surrogate models + # for advance ratio j between 0.10 and 0.20, linearly interpolate between the two surrogate models jinterp_min = 0.10 jinterp_max = 0.20 - j = inputs['J'] - cp = inputs['cp'] - nn = self.options['num_nodes'] + j = inputs["J"] + cp = inputs["cp"] + nn = self.options["num_nodes"] - static_idx = np.where(j<=jinterp_min) - dynamic_idx = np.where(j>=jinterp_max) - tmp = np.logical_and(j>jinterp_min, j= jinterp_max) + tmp = np.logical_and(j > jinterp_min, j < jinterp_max) interp_idx = np.where(tmp) dctdcp = np.zeros(nn) - ct1 = inputs['ct_over_cp'] * cp - ct2 = cp * inputs['eta_prop'] / j - dct1dcp = inputs['ct_over_cp'] - dct2dcp = inputs['eta_prop'] / j + ct1 = inputs["ct_over_cp"] * cp + ct2 = cp * inputs["eta_prop"] / j + dct1dcp = inputs["ct_over_cp"] + dct2dcp = inputs["eta_prop"] / j - #if j <= jinterp_min: + # if j <= jinterp_min: dctdcp[static_idx] = dct1dcp[static_idx] - #if j > jinterp_min and < jinterp_max: - interv = np.ones(nn)*jinterp_max - np.ones(nn)*jinterp_min - interp1 = (np.ones(nn)*jinterp_max - j) / interv - interp2 = (j - np.ones(nn)*jinterp_min) / interv + # if j > jinterp_min and < jinterp_max: + interv = np.ones(nn) * jinterp_max - np.ones(nn) * jinterp_min + interp1 = (np.ones(nn) * jinterp_max - j) / interv + interp2 = (j - np.ones(nn) * jinterp_min) / interv dctdcp[interp_idx] = interp1[interp_idx] * dct1dcp[interp_idx] + interp2[interp_idx] * dct2dcp[interp_idx] - #else if j >= jinterp_max + # else if j >= jinterp_max dctdcp[dynamic_idx] = dct2dcp[dynamic_idx] ct = cp * dctdcp @@ -176,78 +200,92 @@ def compute_partials(self, inputs, J): j_thrust_eta_prop = np.zeros(nn) j_thrust_j = np.zeros(nn) - thrust_over_ct = inputs['fltcond|rho'] * (inputs['rpm']/60.)**2 * inputs['diameter']**4 - thrust = ct * inputs['fltcond|rho'] * (inputs['rpm']/60.)**2 * inputs['diameter']**4 + thrust_over_ct = inputs["fltcond|rho"] * (inputs["rpm"] / 60.0) ** 2 * inputs["diameter"] ** 4 + thrust = ct * inputs["fltcond|rho"] * (inputs["rpm"] / 60.0) ** 2 * inputs["diameter"] ** 4 - J['thrust','fltcond|rho'] = thrust / inputs['fltcond|rho'] - J['thrust', 'rpm'] = 2 * thrust / inputs['rpm'] - J['thrust', 'diameter'] = 4 * thrust / inputs['diameter'] - J['thrust', 'cp'] = dctdcp * inputs['fltcond|rho'] * (inputs['rpm']/60.)**2 * inputs['diameter']**4 + J["thrust", "fltcond|rho"] = thrust / inputs["fltcond|rho"] + J["thrust", "rpm"] = 2 * thrust / inputs["rpm"] + J["thrust", "diameter"] = 4 * thrust / inputs["diameter"] + J["thrust", "cp"] = dctdcp * inputs["fltcond|rho"] * (inputs["rpm"] / 60.0) ** 2 * inputs["diameter"] ** 4 - #if j <= jinterp_min: - j_thrust_ct_over_cp[static_idx] = thrust[static_idx] / inputs['ct_over_cp'][static_idx] + # if j <= jinterp_min: + j_thrust_ct_over_cp[static_idx] = thrust[static_idx] / inputs["ct_over_cp"][static_idx] j_thrust_eta_prop[static_idx] = np.zeros(static_idx[0].shape) - j_thrust_j[static_idx] = np.zeros(static_idx[0].shape) + j_thrust_j[static_idx] = np.zeros(static_idx[0].shape) # j < jinterp_max: - j_thrust_ct_over_cp[interp_idx] = thrust_over_ct[interp_idx] * interp1[interp_idx] * (ct1[interp_idx] / inputs['ct_over_cp'][interp_idx]) - j_thrust_eta_prop[interp_idx] = thrust_over_ct[interp_idx] * interp2[interp_idx] * (ct2[interp_idx] / inputs['eta_prop'][interp_idx]) - j_thrust_j[interp_idx] = thrust_over_ct[interp_idx] * (-ct1[interp_idx] / interv[interp_idx] + ct2[interp_idx] / interv[interp_idx] - interp2[interp_idx] * ct2[interp_idx] / j[interp_idx]) - - #else: + j_thrust_ct_over_cp[interp_idx] = ( + thrust_over_ct[interp_idx] * interp1[interp_idx] * (ct1[interp_idx] / inputs["ct_over_cp"][interp_idx]) + ) + j_thrust_eta_prop[interp_idx] = ( + thrust_over_ct[interp_idx] * interp2[interp_idx] * (ct2[interp_idx] / inputs["eta_prop"][interp_idx]) + ) + j_thrust_j[interp_idx] = thrust_over_ct[interp_idx] * ( + -ct1[interp_idx] / interv[interp_idx] + + ct2[interp_idx] / interv[interp_idx] + - interp2[interp_idx] * ct2[interp_idx] / j[interp_idx] + ) + + # else: j_thrust_ct_over_cp[dynamic_idx] = np.zeros(dynamic_idx[0].shape) - j_thrust_eta_prop[dynamic_idx] = thrust[dynamic_idx] / inputs['eta_prop'][dynamic_idx] - j_thrust_j[dynamic_idx] = - thrust[dynamic_idx] / j[dynamic_idx] + j_thrust_eta_prop[dynamic_idx] = thrust[dynamic_idx] / inputs["eta_prop"][dynamic_idx] + j_thrust_j[dynamic_idx] = -thrust[dynamic_idx] / j[dynamic_idx] + + J["thrust", "ct_over_cp"] = j_thrust_ct_over_cp + J["thrust", "eta_prop"] = j_thrust_eta_prop + J["thrust", "J"] = j_thrust_j - J['thrust','ct_over_cp'] = j_thrust_ct_over_cp - J['thrust','eta_prop'] = j_thrust_eta_prop - J['thrust','J'] = j_thrust_j class PropCoefficients(ExplicitComponent): def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('shaft_power_in', units='W', desc='Input shaft power',shape=(nn,), val=5*np.ones(nn)) - self.add_input('diameter', units='m', val=2.5, desc='Prop diameter') - self.add_input('rpm', units='rpm', val=2500*np.ones(nn), desc='Propeller shaft speed') - self.add_input('fltcond|rho', units='kg / m ** 3',desc='Air density',shape=(nn,)) - self.add_input('fltcond|Utrue', units='m/s', desc='Flight speed',shape=(nn,)) - - #outputs and partials - self.add_output('cp', desc='Power coefficient', val=0.1*np.ones(nn), lower=np.zeros(nn),upper=np.ones(nn)*2.4) - self.add_output('J', desc='Advance ratio', val=0.2**np.ones(nn), lower=1e-4*np.ones(nn), upper=4.0*np.ones(nn)) - self.add_output('prop_Vtip',desc='Propeller tip speed',shape=(nn,)) - - self.declare_partials('cp','diameter') - self.declare_partials('cp','shaft_power_in', rows=range(nn), cols=range(nn)) - self.declare_partials('cp', ['fltcond|rho','rpm'], rows=range(nn), cols=range(nn)) - self.declare_partials('J','diameter') - self.declare_partials('J', ['fltcond|Utrue','rpm'], rows=range(nn), cols=range(nn)) - self.declare_partials('prop_Vtip','rpm', rows=range(nn), cols=range(nn)) - self.declare_partials('prop_Vtip','diameter') + nn = self.options["num_nodes"] + self.add_input("shaft_power_in", units="W", desc="Input shaft power", shape=(nn,), val=5 * np.ones(nn)) + self.add_input("diameter", units="m", val=2.5, desc="Prop diameter") + self.add_input("rpm", units="rpm", val=2500 * np.ones(nn), desc="Propeller shaft speed") + self.add_input("fltcond|rho", units="kg / m ** 3", desc="Air density", shape=(nn,)) + self.add_input("fltcond|Utrue", units="m/s", desc="Flight speed", shape=(nn,)) + + # outputs and partials + self.add_output( + "cp", desc="Power coefficient", val=0.1 * np.ones(nn), lower=np.zeros(nn), upper=np.ones(nn) * 2.4 + ) + self.add_output( + "J", desc="Advance ratio", val=0.2 ** np.ones(nn), lower=1e-4 * np.ones(nn), upper=4.0 * np.ones(nn) + ) + self.add_output("prop_Vtip", desc="Propeller tip speed", shape=(nn,)) + + self.declare_partials("cp", "diameter") + self.declare_partials("cp", "shaft_power_in", rows=range(nn), cols=range(nn)) + self.declare_partials("cp", ["fltcond|rho", "rpm"], rows=range(nn), cols=range(nn)) + self.declare_partials("J", "diameter") + self.declare_partials("J", ["fltcond|Utrue", "rpm"], rows=range(nn), cols=range(nn)) + self.declare_partials("prop_Vtip", "rpm", rows=range(nn), cols=range(nn)) + self.declare_partials("prop_Vtip", "diameter") def compute(self, inputs, outputs): - #print('Prop shaft power input: ' + str(inputs['shaft_power_in'])) - outputs['cp'] = inputs['shaft_power_in']/inputs['fltcond|rho'] / (inputs['rpm']/60)**3 / inputs['diameter']**5 - #print('cp: '+str(outputs['cp'])) - outputs['J'] = 60. * inputs['fltcond|Utrue'] / inputs['rpm'] / inputs['diameter'] - #print('U:'+str(inputs['fltcond|Utrue'])) - #print('J: '+str(outputs['J'])) - outputs['prop_Vtip'] = inputs['rpm'] / 60 * np.pi * inputs['diameter'] + # print('Prop shaft power input: ' + str(inputs['shaft_power_in'])) + outputs["cp"] = ( + inputs["shaft_power_in"] / inputs["fltcond|rho"] / (inputs["rpm"] / 60) ** 3 / inputs["diameter"] ** 5 + ) + # print('cp: '+str(outputs['cp'])) + outputs["J"] = 60.0 * inputs["fltcond|Utrue"] / inputs["rpm"] / inputs["diameter"] + # print('U:'+str(inputs['fltcond|Utrue'])) + # print('J: '+str(outputs['J'])) + outputs["prop_Vtip"] = inputs["rpm"] / 60 * np.pi * inputs["diameter"] def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - cpval = inputs['shaft_power_in']/inputs['fltcond|rho'] / (inputs['rpm']/60)**3 / inputs['diameter']**5 - jval = 60. * inputs['fltcond|Utrue'] / inputs['rpm'] / inputs['diameter'] - J['cp','shaft_power_in'] = 1 /inputs['fltcond|rho'] / (inputs['rpm']/60)**3 / inputs['diameter']**5 - J['cp','fltcond|rho'] = - cpval / inputs['fltcond|rho'] - J['cp','rpm'] = -3. * cpval / inputs['rpm'] - J['cp','diameter'] = -5. * cpval / inputs['diameter'] - J['J','fltcond|Utrue'] = jval / inputs['fltcond|Utrue'] - J['J','rpm'] = - jval / inputs['rpm'] - J['J','diameter'] = - jval / inputs['diameter'] - J['prop_Vtip','rpm'] = 1 / 60 * np.pi * inputs['diameter'] *np.ones(nn) - J['prop_Vtip','diameter'] = inputs['rpm'] / 60 * np.pi - + nn = self.options["num_nodes"] + cpval = inputs["shaft_power_in"] / inputs["fltcond|rho"] / (inputs["rpm"] / 60) ** 3 / inputs["diameter"] ** 5 + jval = 60.0 * inputs["fltcond|Utrue"] / inputs["rpm"] / inputs["diameter"] + J["cp", "shaft_power_in"] = 1 / inputs["fltcond|rho"] / (inputs["rpm"] / 60) ** 3 / inputs["diameter"] ** 5 + J["cp", "fltcond|rho"] = -cpval / inputs["fltcond|rho"] + J["cp", "rpm"] = -3.0 * cpval / inputs["rpm"] + J["cp", "diameter"] = -5.0 * cpval / inputs["diameter"] + J["J", "fltcond|Utrue"] = jval / inputs["fltcond|Utrue"] + J["J", "rpm"] = -jval / inputs["rpm"] + J["J", "diameter"] = -jval / inputs["diameter"] + J["prop_Vtip", "rpm"] = 1 / 60 * np.pi * inputs["diameter"] * np.ones(nn) + J["prop_Vtip", "diameter"] = inputs["rpm"] / 60 * np.pi diff --git a/openconcept/propulsion/splitter.py b/openconcept/propulsion/splitter.py index bdd5c5fe..4123e0f5 100644 --- a/openconcept/propulsion/splitter.py +++ b/openconcept/propulsion/splitter.py @@ -58,108 +58,102 @@ class PowerSplit(ExplicitComponent): Base cost (default 0 USD) """ + def initialize(self): # define control rules - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('rule', default='fraction', - desc='Control strategy - fraction or fixed power') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("rule", default="fraction", desc="Control strategy - fraction or fixed power") - self.options.declare('efficiency', default=1., desc='Efficiency (dimensionless)') - self.options.declare('weight_inc', default=0., desc='kg per input watt') - self.options.declare('weight_base', default=0., desc='kg base weight') - self.options.declare('cost_inc', default=0., desc='$ cost per input watt') - self.options.declare('cost_base', default=0., desc='$ cost base') + self.options.declare("efficiency", default=1.0, desc="Efficiency (dimensionless)") + self.options.declare("weight_inc", default=0.0, desc="kg per input watt") + self.options.declare("weight_base", default=0.0, desc="kg base weight") + self.options.declare("cost_inc", default=0.0, desc="$ cost per input watt") + self.options.declare("cost_base", default=0.0, desc="$ cost base") def setup(self): - nn = self.options['num_nodes'] - self.add_input('power_in', units='W', - desc='Input shaft power or incoming electrical load', shape=(nn,)) - self.add_input('power_rating', val=99999999, units='W', desc='Split mechanism power rating') + nn = self.options["num_nodes"] + self.add_input("power_in", units="W", desc="Input shaft power or incoming electrical load", shape=(nn,)) + self.add_input("power_rating", val=99999999, units="W", desc="Split mechanism power rating") - rule = self.options['rule'] - if rule == 'fraction': - self.add_input('power_split_fraction', val=0.5, - desc='Fraction of power to output A', shape=(nn,)) - elif rule == 'fixed': - self.add_input('power_split_amount', units='W', - desc='Raw amount of power to output A', shape=(nn,)) + rule = self.options["rule"] + if rule == "fraction": + self.add_input("power_split_fraction", val=0.5, desc="Fraction of power to output A", shape=(nn,)) + elif rule == "fixed": + self.add_input("power_split_amount", units="W", desc="Raw amount of power to output A", shape=(nn,)) else: msg = 'Specify either "fraction" or "fixed" as power split control rule' raise ValueError(msg) - eta = self.options['efficiency'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] + eta = self.options["efficiency"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] - self.add_output('power_out_A', units='W', desc='Output power or load to A', shape=(nn,)) - self.add_output('power_out_B', units='W', desc='Output power or load to B', shape=(nn,)) - self.add_output('heat_out', units='W', desc='Waste heat out', shape=(nn,)) - self.add_output('component_cost', units='USD', desc='Splitter component cost') - self.add_output('component_weight', units='kg', desc='Splitter component weight') - self.add_output('component_sizing_margin', desc='Fraction of rated power', shape=(nn,)) + self.add_output("power_out_A", units="W", desc="Output power or load to A", shape=(nn,)) + self.add_output("power_out_B", units="W", desc="Output power or load to B", shape=(nn,)) + self.add_output("heat_out", units="W", desc="Waste heat out", shape=(nn,)) + self.add_output("component_cost", units="USD", desc="Splitter component cost") + self.add_output("component_weight", units="kg", desc="Splitter component weight") + self.add_output("component_sizing_margin", desc="Fraction of rated power", shape=(nn,)) - if rule == 'fraction': - self.declare_partials(['power_out_A', 'power_out_B'], - ['power_in', 'power_split_fraction'], - rows=range(nn), cols=range(nn)) - elif rule == 'fixed': - self.declare_partials(['power_out_A', 'power_out_B'], - ['power_in', 'power_split_amount'], - rows=range(nn), cols=range(nn)) - self.declare_partials('heat_out', 'power_in', val=(1 - eta) * np.ones(nn), - rows=range(nn), cols=range(nn)) - self.declare_partials('component_cost', 'power_rating', val=cost_inc) - self.declare_partials('component_weight', 'power_rating', val=weight_inc) - self.declare_partials('component_sizing_margin', 'power_in', - rows=range(nn), cols=range(nn)) - self.declare_partials('component_sizing_margin', 'power_rating') + if rule == "fraction": + self.declare_partials( + ["power_out_A", "power_out_B"], ["power_in", "power_split_fraction"], rows=range(nn), cols=range(nn) + ) + elif rule == "fixed": + self.declare_partials( + ["power_out_A", "power_out_B"], ["power_in", "power_split_amount"], rows=range(nn), cols=range(nn) + ) + self.declare_partials("heat_out", "power_in", val=(1 - eta) * np.ones(nn), rows=range(nn), cols=range(nn)) + self.declare_partials("component_cost", "power_rating", val=cost_inc) + self.declare_partials("component_weight", "power_rating", val=weight_inc) + self.declare_partials("component_sizing_margin", "power_in", rows=range(nn), cols=range(nn)) + self.declare_partials("component_sizing_margin", "power_rating") def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - rule = self.options['rule'] - eta = self.options['efficiency'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] + nn = self.options["num_nodes"] + rule = self.options["rule"] + eta = self.options["efficiency"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] - if rule == 'fraction': - outputs['power_out_A'] = inputs['power_in'] * inputs['power_split_fraction'] * eta - outputs['power_out_B'] = inputs['power_in'] * (1 - inputs['power_split_fraction']) * eta - elif rule == 'fixed': + if rule == "fraction": + outputs["power_out_A"] = inputs["power_in"] * inputs["power_split_fraction"] * eta + outputs["power_out_B"] = inputs["power_in"] * (1 - inputs["power_split_fraction"]) * eta + elif rule == "fixed": # check to make sure enough power is available # if inputs['power_in'] < inputs['power_split_amount']: - not_enough_idx = np.where(inputs['power_in'] < inputs['power_split_amount']) + not_enough_idx = np.where(inputs["power_in"] < inputs["power_split_amount"]) po_A = np.zeros(nn) po_B = np.zeros(nn) - po_A[not_enough_idx] = inputs['power_in'][not_enough_idx] * eta + po_A[not_enough_idx] = inputs["power_in"][not_enough_idx] * eta po_B[not_enough_idx] = np.zeros(nn)[not_enough_idx] # else: - enough_idx = np.where(inputs['power_in'] >= inputs['power_split_amount']) - po_A[enough_idx] = inputs['power_split_amount'][enough_idx] * eta - po_B[enough_idx] = (inputs['power_in'][enough_idx] - - inputs['power_split_amount'][enough_idx]) * eta - outputs['power_out_A'] = po_A - outputs['power_out_B'] = po_B - outputs['heat_out'] = inputs['power_in'] * (1 - eta) - outputs['component_cost'] = inputs['power_rating'] * cost_inc + cost_base - outputs['component_weight'] = inputs['power_rating'] * weight_inc + weight_base - outputs['component_sizing_margin'] = inputs['power_in'] / inputs['power_rating'] + enough_idx = np.where(inputs["power_in"] >= inputs["power_split_amount"]) + po_A[enough_idx] = inputs["power_split_amount"][enough_idx] * eta + po_B[enough_idx] = (inputs["power_in"][enough_idx] - inputs["power_split_amount"][enough_idx]) * eta + outputs["power_out_A"] = po_A + outputs["power_out_B"] = po_B + outputs["heat_out"] = inputs["power_in"] * (1 - eta) + outputs["component_cost"] = inputs["power_rating"] * cost_inc + cost_base + outputs["component_weight"] = inputs["power_rating"] * weight_inc + weight_base + outputs["component_sizing_margin"] = inputs["power_in"] / inputs["power_rating"] def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - rule = self.options['rule'] - eta = self.options['efficiency'] - if rule == 'fraction': - J['power_out_A', 'power_in'] = inputs['power_split_fraction'] * eta - J['power_out_A', 'power_split_fraction'] = inputs['power_in'] * eta - J['power_out_B', 'power_in'] = (1 - inputs['power_split_fraction']) * eta - J['power_out_B', 'power_split_fraction'] = -inputs['power_in'] * eta - elif rule == 'fixed': - not_enough_idx = np.where(inputs['power_in'] < inputs['power_split_amount']) - enough_idx = np.where(inputs['power_in'] >= inputs['power_split_amount']) + nn = self.options["num_nodes"] + rule = self.options["rule"] + eta = self.options["efficiency"] + if rule == "fraction": + J["power_out_A", "power_in"] = inputs["power_split_fraction"] * eta + J["power_out_A", "power_split_fraction"] = inputs["power_in"] * eta + J["power_out_B", "power_in"] = (1 - inputs["power_split_fraction"]) * eta + J["power_out_B", "power_split_fraction"] = -inputs["power_in"] * eta + elif rule == "fixed": + not_enough_idx = np.where(inputs["power_in"] < inputs["power_split_amount"]) + enough_idx = np.where(inputs["power_in"] >= inputs["power_split_amount"]) # if inputs['power_in'] < inputs['power_split_amount']: Jpo_A_pi = np.zeros(nn) Jpo_A_ps = np.zeros(nn) @@ -174,10 +168,9 @@ def compute_partials(self, inputs, J): Jpo_A_pi[enough_idx] = np.zeros(nn)[enough_idx] Jpo_B_ps[enough_idx] = -eta * np.ones(nn)[enough_idx] Jpo_B_pi[enough_idx] = eta * np.ones(nn)[enough_idx] - J['power_out_A', 'power_in'] = Jpo_A_pi - J['power_out_A', 'power_split_amount'] = Jpo_A_ps - J['power_out_B', 'power_in'] = Jpo_B_pi - J['power_out_B', 'power_split_amount'] = Jpo_B_ps - J['component_sizing_margin', 'power_in'] = 1 / inputs['power_rating'] - J['component_sizing_margin', 'power_rating'] = - (inputs['power_in'] / - inputs['power_rating'] ** 2) + J["power_out_A", "power_in"] = Jpo_A_pi + J["power_out_A", "power_split_amount"] = Jpo_A_ps + J["power_out_B", "power_in"] = Jpo_B_pi + J["power_out_B", "power_split_amount"] = Jpo_B_ps + J["component_sizing_margin", "power_in"] = 1 / inputs["power_rating"] + J["component_sizing_margin", "power_rating"] = -(inputs["power_in"] / inputs["power_rating"] ** 2) diff --git a/openconcept/propulsion/systems/simple_all_electric.py b/openconcept/propulsion/systems/simple_all_electric.py index 7417bd21..0800efa3 100644 --- a/openconcept/propulsion/systems/simple_all_electric.py +++ b/openconcept/propulsion/systems/simple_all_electric.py @@ -2,178 +2,204 @@ from openconcept.energy_storage import SOCBattery from openconcept.utilities import DVLabel from openmdao.api import Group, IndepVarComp -from openconcept.thermal import LiquidCooledComp, CoolantReservoir, ImplicitCompressibleDuct, ExplicitIncompressibleDuct, HXGroup +from openconcept.thermal import ( + LiquidCooledComp, + CoolantReservoir, + ImplicitCompressibleDuct, + ExplicitIncompressibleDuct, + HXGroup, +) import numpy as np + + class AllElectricSinglePropulsionSystemWithThermal_Compressible(Group): """This is an example model of the an electric propulsion system - consisting of a constant-speed prop, motor, and battery. - Thermal management is provided using a compressible 1D duct - with heat exchanger. - - Inputs - ------ - ac|propulsion|motor|rating : float - The maximum rated continuous shaft power of the motor - ac|propulsion|propeller|diameter : float - Diameter of the propeller - ac|weights|W_battery : float - Battery weight - - Options - ------- - num_nodes : float - Number of analysis points to run (default 1) + consisting of a constant-speed prop, motor, and battery. + Thermal management is provided using a compressible 1D duct + with heat exchanger. + + Inputs + ------ + ac|propulsion|motor|rating : float + The maximum rated continuous shaft power of the motor + ac|propulsion|propeller|diameter : float + Diameter of the propeller + ac|weights|W_battery : float + Battery weight + + Options + ------- + num_nodes : float + Number of analysis points to run (default 1) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc="Number of mission analysis points to run") - self.options.declare('specific_energy', default=300, desc="Battery spec energy in Wh/kg") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") + self.options.declare("specific_energy", default=300, desc="Battery spec energy in Wh/kg") + def setup(self): - nn = self.options['num_nodes'] - e_b = self.options['specific_energy'] + nn = self.options["num_nodes"] + e_b = self.options["specific_energy"] # rename incoming design variables - dvlist = [['ac|propulsion|motor|rating', 'motor1_rating', 850, 'hp'], - ['ac|propulsion|propeller|diameter', 'prop1_diameter', 2.3, 'm'], - ['ac|weights|W_battery','battery1_weight',300,'kg']] - self.add_subsystem('dvs', DVLabel(dvlist), - promotes_inputs=["*"], promotes_outputs=["*"]) + dvlist = [ + ["ac|propulsion|motor|rating", "motor1_rating", 850, "hp"], + ["ac|propulsion|propeller|diameter", "prop1_diameter", 2.3, "m"], + ["ac|weights|W_battery", "battery1_weight", 300, "kg"], + ] + self.add_subsystem("dvs", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) # introduce model components - self.add_subsystem('motor1', - SimpleMotor(num_nodes=nn, weight_inc=1/5000, weight_base=0, efficiency=0.97), - promotes_inputs=["throttle"]) - self.add_subsystem('prop1', - SimplePropeller(num_nodes=nn, num_blades=4, - design_J=2.2, design_cp=0.55), - promotes_inputs=["fltcond|*"], promotes_outputs=["thrust"]) - self.add_subsystem('batt1', SOCBattery(num_nodes=nn, specific_energy=e_b, efficiency=0.97),promotes_inputs=["duration"]) + self.add_subsystem( + "motor1", + SimpleMotor(num_nodes=nn, weight_inc=1 / 5000, weight_base=0, efficiency=0.97), + promotes_inputs=["throttle"], + ) + self.add_subsystem( + "prop1", + SimplePropeller(num_nodes=nn, num_blades=4, design_J=2.2, design_cp=0.55), + promotes_inputs=["fltcond|*"], + promotes_outputs=["thrust"], + ) + self.add_subsystem( + "batt1", SOCBattery(num_nodes=nn, specific_energy=e_b, efficiency=0.97), promotes_inputs=["duration"] + ) # connect design variables to model component inputs - self.connect('motor1_rating', 'motor1.elec_power_rating') - self.connect('motor1_rating', 'prop1.power_rating') - self.connect('prop1_diameter', 'prop1.diameter') - self.connect('battery1_weight','batt1.battery_weight') + self.connect("motor1_rating", "motor1.elec_power_rating") + self.connect("motor1_rating", "prop1.power_rating") + self.connect("prop1_diameter", "prop1.diameter") + self.connect("battery1_weight", "batt1.battery_weight") # connect components to each other - self.connect('motor1.shaft_power_out', 'prop1.shaft_power_in') - self.connect('motor1.elec_load', 'batt1.elec_load') - - iv = self.add_subsystem('iv',IndepVarComp(), promotes_outputs=['*']) - iv.add_output('mdot_coolant', val=0.1*np.ones((nn,)), units='kg/s') - iv.add_output('rho_coolant', val=997*np.ones((nn,)),units='kg/m**3') - iv.add_output('coolant_mass', val=10., units='kg') - - iv.add_output('channel_width', val=1, units='mm') - iv.add_output('channel_height', val=20, units='mm') - iv.add_output('channel_length', val=0.2, units='m') - iv.add_output('n_parallel', val=50) - - - lc_promotes = ['duration','channel_*','n_parallel'] - self.add_subsystem('motorheatsink', - LiquidCooledComp(num_nodes=nn, - quasi_steady=False), - promotes_inputs=lc_promotes) - self.connect('motor1.heat_out','motorheatsink.q_in') - self.connect('motor1.component_weight','motorheatsink.mass') - - self.add_subsystem('duct', - ImplicitCompressibleDuct(num_nodes=nn), - promotes_inputs=[('p_inf','fltcond|p'),('T_inf','fltcond|T'),('Utrue','fltcond|Utrue')]) - - self.connect('motorheatsink.T_out','duct.T_in_hot') - self.connect('rho_coolant','duct.rho_hot') - - self.add_subsystem('reservoir', - CoolantReservoir(num_nodes=nn), - promotes_inputs=['duration',('mass','coolant_mass')]) - self.connect('duct.T_out_hot','reservoir.T_in') - self.connect('reservoir.T_out','motorheatsink.T_in') - self.connect('mdot_coolant',['motorheatsink.mdot_coolant','duct.mdot_hot','reservoir.mdot_coolant']) + self.connect("motor1.shaft_power_out", "prop1.shaft_power_in") + self.connect("motor1.elec_load", "batt1.elec_load") + + iv = self.add_subsystem("iv", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("mdot_coolant", val=0.1 * np.ones((nn,)), units="kg/s") + iv.add_output("rho_coolant", val=997 * np.ones((nn,)), units="kg/m**3") + iv.add_output("coolant_mass", val=10.0, units="kg") + + iv.add_output("channel_width", val=1, units="mm") + iv.add_output("channel_height", val=20, units="mm") + iv.add_output("channel_length", val=0.2, units="m") + iv.add_output("n_parallel", val=50) + + lc_promotes = ["duration", "channel_*", "n_parallel"] + self.add_subsystem( + "motorheatsink", LiquidCooledComp(num_nodes=nn, quasi_steady=False), promotes_inputs=lc_promotes + ) + self.connect("motor1.heat_out", "motorheatsink.q_in") + self.connect("motor1.component_weight", "motorheatsink.mass") + + self.add_subsystem( + "duct", + ImplicitCompressibleDuct(num_nodes=nn), + promotes_inputs=[("p_inf", "fltcond|p"), ("T_inf", "fltcond|T"), ("Utrue", "fltcond|Utrue")], + ) + + self.connect("motorheatsink.T_out", "duct.T_in_hot") + self.connect("rho_coolant", "duct.rho_hot") + + self.add_subsystem( + "reservoir", CoolantReservoir(num_nodes=nn), promotes_inputs=["duration", ("mass", "coolant_mass")] + ) + self.connect("duct.T_out_hot", "reservoir.T_in") + self.connect("reservoir.T_out", "motorheatsink.T_in") + self.connect("mdot_coolant", ["motorheatsink.mdot_coolant", "duct.mdot_hot", "reservoir.mdot_coolant"]) + class AllElectricSinglePropulsionSystemWithThermal_Incompressible(Group): """This is an example model of the an electric propulsion system - consisting of a constant-speed prop, motor, and battery. - Thermal management is provided using a incompressible - approximation of a 1D duct with heat exchanger. - - Inputs - ------ - ac|propulsion|motor|rating : float - The maximum rated continuous shaft power of the motor - ac|propulsion|propeller|diameter : float - Diameter of the propeller - ac|weights|W_battery : float - Battery weight - - Options - ------- - num_nodes : float - Number of analysis points to run (default 1) + consisting of a constant-speed prop, motor, and battery. + Thermal management is provided using a incompressible + approximation of a 1D duct with heat exchanger. + + Inputs + ------ + ac|propulsion|motor|rating : float + The maximum rated continuous shaft power of the motor + ac|propulsion|propeller|diameter : float + Diameter of the propeller + ac|weights|W_battery : float + Battery weight + + Options + ------- + num_nodes : float + Number of analysis points to run (default 1) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc="Number of mission analysis points to run") - self.options.declare('specific_energy', default=300, desc="Battery spec energy in Wh/kg") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") + self.options.declare("specific_energy", default=300, desc="Battery spec energy in Wh/kg") + def setup(self): - nn = self.options['num_nodes'] - e_b = self.options['specific_energy'] + nn = self.options["num_nodes"] + e_b = self.options["specific_energy"] # rename incoming design variables - dvlist = [['ac|propulsion|motor|rating', 'motor1_rating', 850, 'hp'], - ['ac|propulsion|propeller|diameter', 'prop1_diameter', 2.3, 'm'], - ['ac|weights|W_battery','battery1_weight',300,'kg']] - self.add_subsystem('dvs', DVLabel(dvlist), - promotes_inputs=["*"], promotes_outputs=["*"]) + dvlist = [ + ["ac|propulsion|motor|rating", "motor1_rating", 850, "hp"], + ["ac|propulsion|propeller|diameter", "prop1_diameter", 2.3, "m"], + ["ac|weights|W_battery", "battery1_weight", 300, "kg"], + ] + self.add_subsystem("dvs", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) # introduce model components - self.add_subsystem('motor1', - SimpleMotor(num_nodes=nn, weight_inc=1/5000, weight_base=0, efficiency=0.97), - promotes_inputs=["throttle"]) - self.add_subsystem('prop1', - SimplePropeller(num_nodes=nn, num_blades=4, - design_J=2.2, design_cp=0.55), - promotes_inputs=["fltcond|*"], promotes_outputs=["thrust"]) - self.add_subsystem('batt1', SOCBattery(num_nodes=nn, specific_energy=e_b, efficiency=0.97),promotes_inputs=["duration"]) + self.add_subsystem( + "motor1", + SimpleMotor(num_nodes=nn, weight_inc=1 / 5000, weight_base=0, efficiency=0.97), + promotes_inputs=["throttle"], + ) + self.add_subsystem( + "prop1", + SimplePropeller(num_nodes=nn, num_blades=4, design_J=2.2, design_cp=0.55), + promotes_inputs=["fltcond|*"], + promotes_outputs=["thrust"], + ) + self.add_subsystem( + "batt1", SOCBattery(num_nodes=nn, specific_energy=e_b, efficiency=0.97), promotes_inputs=["duration"] + ) # connect design variables to model component inputs - self.connect('motor1_rating', 'motor1.elec_power_rating') - self.connect('motor1_rating', 'prop1.power_rating') - self.connect('prop1_diameter', 'prop1.diameter') - self.connect('battery1_weight','batt1.battery_weight') + self.connect("motor1_rating", "motor1.elec_power_rating") + self.connect("motor1_rating", "prop1.power_rating") + self.connect("prop1_diameter", "prop1.diameter") + self.connect("battery1_weight", "batt1.battery_weight") # connect components to each other - self.connect('motor1.shaft_power_out', 'prop1.shaft_power_in') - self.connect('motor1.elec_load', 'batt1.elec_load') - - iv = self.add_subsystem('iv',IndepVarComp(), promotes_outputs=['*']) - iv.add_output('mdot_coolant', val=0.1*np.ones((nn,)), units='kg/s') - iv.add_output('rho_coolant', val=997*np.ones((nn,)),units='kg/m**3') - iv.add_output('coolant_mass', val=10., units='kg') - - iv.add_output('channel_width', val=1, units='mm') - iv.add_output('channel_height', val=20, units='mm') - iv.add_output('channel_length', val=0.2, units='m') - iv.add_output('n_parallel', val=50) - iv.add_output('area_nozzle', val=58*np.ones((nn,)), units='inch**2') - - lc_promotes = ['duration','channel_*','n_parallel'] - self.add_subsystem('motorheatsink', - LiquidCooledComp(num_nodes=nn, - quasi_steady=False), - promotes_inputs=lc_promotes) - self.connect('motor1.heat_out','motorheatsink.q_in') - self.connect('motor1.component_weight','motorheatsink.mass') - self.add_subsystem('duct', - ExplicitIncompressibleDuct(num_nodes=nn), - promotes_inputs=['fltcond|*']) - self.connect('area_nozzle','duct.area_nozzle') - self.add_subsystem('hx',HXGroup(num_nodes=nn),promotes_inputs=[('T_in_cold','fltcond|T'),('rho_cold','fltcond|rho')]) - self.connect('duct.mdot','hx.mdot_cold') - self.connect('hx.delta_p_cold','duct.delta_p_hex') - - self.connect('motorheatsink.T_out','hx.T_in_hot') - self.connect('rho_coolant','hx.rho_hot') - - self.add_subsystem('reservoir', - CoolantReservoir(num_nodes=nn), - promotes_inputs=['duration',('mass','coolant_mass')]) - self.connect('hx.T_out_hot','reservoir.T_in') - self.connect('reservoir.T_out','motorheatsink.T_in') - self.connect('mdot_coolant',['motorheatsink.mdot_coolant','hx.mdot_hot','reservoir.mdot_coolant']) \ No newline at end of file + self.connect("motor1.shaft_power_out", "prop1.shaft_power_in") + self.connect("motor1.elec_load", "batt1.elec_load") + + iv = self.add_subsystem("iv", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("mdot_coolant", val=0.1 * np.ones((nn,)), units="kg/s") + iv.add_output("rho_coolant", val=997 * np.ones((nn,)), units="kg/m**3") + iv.add_output("coolant_mass", val=10.0, units="kg") + + iv.add_output("channel_width", val=1, units="mm") + iv.add_output("channel_height", val=20, units="mm") + iv.add_output("channel_length", val=0.2, units="m") + iv.add_output("n_parallel", val=50) + iv.add_output("area_nozzle", val=58 * np.ones((nn,)), units="inch**2") + + lc_promotes = ["duration", "channel_*", "n_parallel"] + self.add_subsystem( + "motorheatsink", LiquidCooledComp(num_nodes=nn, quasi_steady=False), promotes_inputs=lc_promotes + ) + self.connect("motor1.heat_out", "motorheatsink.q_in") + self.connect("motor1.component_weight", "motorheatsink.mass") + self.add_subsystem("duct", ExplicitIncompressibleDuct(num_nodes=nn), promotes_inputs=["fltcond|*"]) + self.connect("area_nozzle", "duct.area_nozzle") + self.add_subsystem( + "hx", HXGroup(num_nodes=nn), promotes_inputs=[("T_in_cold", "fltcond|T"), ("rho_cold", "fltcond|rho")] + ) + self.connect("duct.mdot", "hx.mdot_cold") + self.connect("hx.delta_p_cold", "duct.delta_p_hex") + + self.connect("motorheatsink.T_out", "hx.T_in_hot") + self.connect("rho_coolant", "hx.rho_hot") + + self.add_subsystem( + "reservoir", CoolantReservoir(num_nodes=nn), promotes_inputs=["duration", ("mass", "coolant_mass")] + ) + self.connect("hx.T_out_hot", "reservoir.T_in") + self.connect("reservoir.T_out", "motorheatsink.T_in") + self.connect("mdot_coolant", ["motorheatsink.mdot_coolant", "hx.mdot_hot", "reservoir.mdot_coolant"]) diff --git a/openconcept/propulsion/systems/simple_series_hybrid.py b/openconcept/propulsion/systems/simple_series_hybrid.py index 7e107127..26cdd0d6 100644 --- a/openconcept/propulsion/systems/simple_series_hybrid.py +++ b/openconcept/propulsion/systems/simple_series_hybrid.py @@ -1,4 +1,3 @@ - from openconcept.propulsion import SimpleMotor, PowerSplit, SimpleGenerator, SimpleTurboshaft, SimplePropeller from openconcept.energy_storage import SimpleBattery, SOCBattery from openconcept.utilities import DVLabel, AddSubtractComp, ElementMultiplyDivideComp @@ -39,83 +38,108 @@ class TwinSeriesHybridElectricPropulsionSystem(Group): fuel_flow (integrate over time) elec_load (integrate over time to obtain SOC) motor2.throttle """ - def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + def initialize(self): + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] - - #define design variables that are independent of flight condition or control states - dvlist = [['ac|propulsion|engine|rating','eng_rating',260.0,'kW'], - ['ac|propulsion|propeller|diameter','prop_diameter',2.5,'m'], - ['ac|propulsion|motor|rating','motor_rating',240.0,'kW'], - ['ac|propulsion|generator|rating','gen_rating',250.0,'kW'], - ['ac|weights|W_battery','batt_weight',2000,'kg'], - ['ac|propulsion|battery|specific_energy','specific_energy',300,'W*h/kg'] - ] - - self.add_subsystem('dvs',DVLabel(dvlist),promotes_inputs=["*"],promotes_outputs=["*"]) - #introduce model components - self.add_subsystem('motor1', SimpleMotor(efficiency=0.97,num_nodes=nn),promotes_inputs=['throttle']) - self.add_subsystem('prop1',SimplePropeller(num_nodes=nn),promotes_inputs=["fltcond|*"]) - self.connect('motor1.shaft_power_out','prop1.shaft_power_in') - - #propulsion models expect a high-level 'throttle' parameter and a 'propulsor_active' flag to set individual throttles - failedengine = ElementMultiplyDivideComp() - failedengine.add_equation('motor2throttle',input_names=['throttle','propulsor_active'],vec_size=nn) - self.add_subsystem('failedmotor', failedengine, - promotes_inputs=['throttle', 'propulsor_active']) - - self.add_subsystem('motor2', SimpleMotor(efficiency=0.97,num_nodes=nn)) - self.add_subsystem('prop2',SimplePropeller(num_nodes=nn),promotes_inputs=["fltcond|*"]) - self.connect('motor2.shaft_power_out','prop2.shaft_power_in') - self.connect('failedmotor.motor2throttle','motor2.throttle') - - - - addpower = AddSubtractComp(output_name='motors_elec_load',input_names=['motor1_elec_load','motor2_elec_load'], units='kW',vec_size=nn) - addpower.add_equation(output_name='thrust',input_names=['prop1_thrust','prop2_thrust'], units='N',vec_size=nn) - self.add_subsystem('add_power',subsys=addpower,promotes_outputs=['*']) - self.connect('motor1.elec_load','add_power.motor1_elec_load') - self.connect('motor2.elec_load','add_power.motor2_elec_load') - self.connect('prop1.thrust','add_power.prop1_thrust') - self.connect('prop2.thrust','add_power.prop2_thrust') - - self.add_subsystem('hybrid_split',PowerSplit(rule='fraction',num_nodes=nn)) - self.connect('motors_elec_load','hybrid_split.power_in') - - self.add_subsystem('eng1',SimpleTurboshaft(num_nodes=nn,weight_inc=0.14/1000,weight_base=104),promotes_outputs=["fuel_flow"]) - self.add_subsystem('gen1',SimpleGenerator(efficiency=0.97,num_nodes=nn)) - - self.connect('eng1.shaft_power_out','gen1.shaft_power_in') - - self.add_subsystem('batt1', SOCBattery(num_nodes=nn, efficiency=0.97),promotes_inputs=["duration", "specific_energy"]) - self.connect('hybrid_split.power_out_A','batt1.elec_load') - self.add_subsystem('eng_throttle_set',BalanceComp(name='eng_throttle', val=np.ones((nn,))*0.5, units=None, eq_units='kW', rhs_name='gen_power_required',lhs_name='gen_power_available')) - self.connect('hybrid_split.power_out_B','eng_throttle_set.gen_power_required') - self.connect('gen1.elec_power_out','eng_throttle_set.gen_power_available') - self.connect('eng_throttle_set.eng_throttle','eng1.throttle') - - addweights = AddSubtractComp(output_name='motors_weight',input_names=['motor1_weight','motor2_weight'], units='kg') - addweights.add_equation(output_name='propellers_weight',input_names=['prop1_weight','prop2_weight'], units='kg') - self.add_subsystem('add_weights',subsys=addweights,promotes_inputs=['*'],promotes_outputs=['*']) - relabel = [['hybrid_split_A_in','battery_load',np.ones(nn)*260.0,'kW']] - self.add_subsystem('relabel',DVLabel(relabel),promotes_outputs=["battery_load"]) - self.connect('hybrid_split.power_out_A','relabel.hybrid_split_A_in') - - self.connect('motor1.component_weight','motor1_weight') - self.connect('motor2.component_weight','motor2_weight') - self.connect('prop1.component_weight','prop1_weight') - self.connect('prop2.component_weight','prop2_weight') - - #connect design variables to model component inputs - self.connect('eng_rating','eng1.shaft_power_rating') - self.connect('prop_diameter',['prop1.diameter','prop2.diameter']) - self.connect('motor_rating',['motor1.elec_power_rating','motor2.elec_power_rating']) - self.connect('motor_rating',['prop1.power_rating','prop2.power_rating']) - self.connect('gen_rating','gen1.elec_power_rating') - self.connect('batt_weight','batt1.battery_weight') + nn = self.options["num_nodes"] + + # define design variables that are independent of flight condition or control states + dvlist = [ + ["ac|propulsion|engine|rating", "eng_rating", 260.0, "kW"], + ["ac|propulsion|propeller|diameter", "prop_diameter", 2.5, "m"], + ["ac|propulsion|motor|rating", "motor_rating", 240.0, "kW"], + ["ac|propulsion|generator|rating", "gen_rating", 250.0, "kW"], + ["ac|weights|W_battery", "batt_weight", 2000, "kg"], + ["ac|propulsion|battery|specific_energy", "specific_energy", 300, "W*h/kg"], + ] + + self.add_subsystem("dvs", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) + # introduce model components + self.add_subsystem("motor1", SimpleMotor(efficiency=0.97, num_nodes=nn), promotes_inputs=["throttle"]) + self.add_subsystem("prop1", SimplePropeller(num_nodes=nn), promotes_inputs=["fltcond|*"]) + self.connect("motor1.shaft_power_out", "prop1.shaft_power_in") + + # propulsion models expect a high-level 'throttle' parameter and a 'propulsor_active' flag to set individual throttles + failedengine = ElementMultiplyDivideComp() + failedengine.add_equation("motor2throttle", input_names=["throttle", "propulsor_active"], vec_size=nn) + self.add_subsystem("failedmotor", failedengine, promotes_inputs=["throttle", "propulsor_active"]) + + self.add_subsystem("motor2", SimpleMotor(efficiency=0.97, num_nodes=nn)) + self.add_subsystem("prop2", SimplePropeller(num_nodes=nn), promotes_inputs=["fltcond|*"]) + self.connect("motor2.shaft_power_out", "prop2.shaft_power_in") + self.connect("failedmotor.motor2throttle", "motor2.throttle") + + addpower = AddSubtractComp( + output_name="motors_elec_load", + input_names=["motor1_elec_load", "motor2_elec_load"], + units="kW", + vec_size=nn, + ) + addpower.add_equation( + output_name="thrust", input_names=["prop1_thrust", "prop2_thrust"], units="N", vec_size=nn + ) + self.add_subsystem("add_power", subsys=addpower, promotes_outputs=["*"]) + self.connect("motor1.elec_load", "add_power.motor1_elec_load") + self.connect("motor2.elec_load", "add_power.motor2_elec_load") + self.connect("prop1.thrust", "add_power.prop1_thrust") + self.connect("prop2.thrust", "add_power.prop2_thrust") + + self.add_subsystem("hybrid_split", PowerSplit(rule="fraction", num_nodes=nn)) + self.connect("motors_elec_load", "hybrid_split.power_in") + + self.add_subsystem( + "eng1", + SimpleTurboshaft(num_nodes=nn, weight_inc=0.14 / 1000, weight_base=104), + promotes_outputs=["fuel_flow"], + ) + self.add_subsystem("gen1", SimpleGenerator(efficiency=0.97, num_nodes=nn)) + + self.connect("eng1.shaft_power_out", "gen1.shaft_power_in") + + self.add_subsystem( + "batt1", SOCBattery(num_nodes=nn, efficiency=0.97), promotes_inputs=["duration", "specific_energy"] + ) + self.connect("hybrid_split.power_out_A", "batt1.elec_load") + self.add_subsystem( + "eng_throttle_set", + BalanceComp( + name="eng_throttle", + val=np.ones((nn,)) * 0.5, + units=None, + eq_units="kW", + rhs_name="gen_power_required", + lhs_name="gen_power_available", + ), + ) + self.connect("hybrid_split.power_out_B", "eng_throttle_set.gen_power_required") + self.connect("gen1.elec_power_out", "eng_throttle_set.gen_power_available") + self.connect("eng_throttle_set.eng_throttle", "eng1.throttle") + + addweights = AddSubtractComp( + output_name="motors_weight", input_names=["motor1_weight", "motor2_weight"], units="kg" + ) + addweights.add_equation( + output_name="propellers_weight", input_names=["prop1_weight", "prop2_weight"], units="kg" + ) + self.add_subsystem("add_weights", subsys=addweights, promotes_inputs=["*"], promotes_outputs=["*"]) + relabel = [["hybrid_split_A_in", "battery_load", np.ones(nn) * 260.0, "kW"]] + self.add_subsystem("relabel", DVLabel(relabel), promotes_outputs=["battery_load"]) + self.connect("hybrid_split.power_out_A", "relabel.hybrid_split_A_in") + + self.connect("motor1.component_weight", "motor1_weight") + self.connect("motor2.component_weight", "motor2_weight") + self.connect("prop1.component_weight", "prop1_weight") + self.connect("prop2.component_weight", "prop2_weight") + + # connect design variables to model component inputs + self.connect("eng_rating", "eng1.shaft_power_rating") + self.connect("prop_diameter", ["prop1.diameter", "prop2.diameter"]) + self.connect("motor_rating", ["motor1.elec_power_rating", "motor2.elec_power_rating"]) + self.connect("motor_rating", ["prop1.power_rating", "prop2.power_rating"]) + self.connect("gen_rating", "gen1.elec_power_rating") + self.connect("batt_weight", "batt1.battery_weight") class SeriesHybridElectricPropulsionSystem(Group): @@ -128,48 +152,52 @@ class SeriesHybridElectricPropulsionSystem(Group): Fuel flows and prop thrust should be fairly accurate. Heat constraints have not yet been incorporated. """ - def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + def initialize(self): + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") def setup(self): - #define design variables that are independent of flight condition or control states - dvlist = [['ac|propulsion|engine|rating','eng1_rating',260.0,'kW'], - ['dv_prop1_diameter','prop1_diameter',2.5,'m'], - ['dv_motor1_rating','motor1_rating',240.0,'kW'], - ['dv_gen1_rating','gen1_rating',250.0,'kW'], - ['dv_batt1_weight','batt1_weight',2000,'kg']] - self.add_subsystem('dvs',DVLabel(dvlist),promotes_inputs=["*"],promotes_outputs=["*"]) - nn = self.options['num_nodes'] - #introduce model components - self.add_subsystem('motor1', SimpleMotor(efficiency=0.97,num_nodes=nn),promotes_inputs=["throttle"]) - self.add_subsystem('hybrid_split',PowerSplit(rule='fraction',num_nodes=nn)) - self.add_subsystem('gen1',SimpleGenerator(efficiency=0.97,num_nodes=nn)) - self.add_subsystem('eng1',SimpleTurboshaft(num_nodes=nn),promotes_outputs=["fuel_flow"]) - self.add_subsystem('batt1',SimpleBattery(num_nodes=nn)) - self.add_subsystem('prop1',SimplePropeller(num_nodes=nn),promotes_inputs=["fltcond_*"],promotes_outputs=["thrust"]) - - #connect design variables to model component inputs - self.connect('eng1_rating','eng1.shaft_power_rating') - self.connect('prop1_diameter','prop1.diameter') - self.connect('motor1_rating','motor1.elec_power_rating') - self.connect('motor1_rating','prop1.power_rating') - self.connect('gen1_rating','gen1.elec_power_rating') - self.connect('batt1_weight','batt1.battery_weight') - - #connect components to each other - self.connect('motor1.shaft_power_out','prop1.shaft_power_in') - self.connect('eng1.shaft_power_out','gen1.shaft_power_in') - self.connect('motor1.elec_load','hybrid_split.power_in') - self.connect('hybrid_split.power_out_A','batt1.elec_load') + # define design variables that are independent of flight condition or control states + dvlist = [ + ["ac|propulsion|engine|rating", "eng1_rating", 260.0, "kW"], + ["dv_prop1_diameter", "prop1_diameter", 2.5, "m"], + ["dv_motor1_rating", "motor1_rating", 240.0, "kW"], + ["dv_gen1_rating", "gen1_rating", 250.0, "kW"], + ["dv_batt1_weight", "batt1_weight", 2000, "kg"], + ] + self.add_subsystem("dvs", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) + nn = self.options["num_nodes"] + # introduce model components + self.add_subsystem("motor1", SimpleMotor(efficiency=0.97, num_nodes=nn), promotes_inputs=["throttle"]) + self.add_subsystem("hybrid_split", PowerSplit(rule="fraction", num_nodes=nn)) + self.add_subsystem("gen1", SimpleGenerator(efficiency=0.97, num_nodes=nn)) + self.add_subsystem("eng1", SimpleTurboshaft(num_nodes=nn), promotes_outputs=["fuel_flow"]) + self.add_subsystem("batt1", SimpleBattery(num_nodes=nn)) + self.add_subsystem( + "prop1", SimplePropeller(num_nodes=nn), promotes_inputs=["fltcond_*"], promotes_outputs=["thrust"] + ) + + # connect design variables to model component inputs + self.connect("eng1_rating", "eng1.shaft_power_rating") + self.connect("prop1_diameter", "prop1.diameter") + self.connect("motor1_rating", "motor1.elec_power_rating") + self.connect("motor1_rating", "prop1.power_rating") + self.connect("gen1_rating", "gen1.elec_power_rating") + self.connect("batt1_weight", "batt1.battery_weight") + + # connect components to each other + self.connect("motor1.shaft_power_out", "prop1.shaft_power_in") + self.connect("eng1.shaft_power_out", "gen1.shaft_power_in") + self.connect("motor1.elec_load", "hybrid_split.power_in") + self.connect("hybrid_split.power_out_A", "batt1.elec_load") # hack = self.add_subsystem('eng1throttlehack',IndepVarComp()) # hack.add_output('throttle',np.ones(nn)) # self.connect('eng1throttlehack.throttle','eng1.throttle') - #there is an implicit gap between gen1 output and split input B - #add implicit component to match gen1 elec output and hybrid_split.power_out_B + # there is an implicit gap between gen1 output and split input B + # add implicit component to match gen1 elec output and hybrid_split.power_out_B # eng1_bal = BalanceComp() # eng1_bal.add_balance('eng1t', val=np.ones(nn)*0.5,eq_units='W') # self.add_subsystem('eng1_control',eng1_bal) @@ -177,9 +205,9 @@ def setup(self): # self.connect('hybrid_split.power_out_B','eng1_control.lhs:eng1t') # self.connect('gen1.elec_power_out','eng1_control.rhs:eng1t') - #self.linear_solver = ScipyKrylov() - #self.nonlinear_solver = NewtonSolver() - #self.nonlinear_solver.options['maxiter'] = 10 + # self.linear_solver = ScipyKrylov() + # self.nonlinear_solver = NewtonSolver() + # self.nonlinear_solver.options['maxiter'] = 10 class SingleSeriesHybridElectricPropulsionSystem(Group): @@ -235,87 +263,90 @@ class SingleSeriesHybridElectricPropulsionSystem(Group): specific_energy : float Battery specific energy (default 300 Wh/kg) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc="Number of mission analysis points to run") - self.options.declare('specific_energy', default=300, desc="Battery spec energy in Wh/kg") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") + self.options.declare("specific_energy", default=300, desc="Battery spec energy in Wh/kg") def setup(self): - nn = self.options['num_nodes'] - e_b = self.options['specific_energy'] + nn = self.options["num_nodes"] + e_b = self.options["specific_energy"] # define design variables that are independent of flight condition or control states - dvlist = [['ac|propulsion|engine|rating', 'eng_rating', 260.0, 'kW'], - ['ac|propulsion|propeller|diameter', 'prop_diameter', 2.5, 'm'], - ['ac|propulsion|motor|rating', 'motor_rating', 240.0, 'kW'], - ['ac|propulsion|generator|rating', 'gen_rating', 250.0, 'kW'], - ['ac|weights|W_battery', 'batt_weight', 2000, 'kg']] - self.add_subsystem('dvs', DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) + dvlist = [ + ["ac|propulsion|engine|rating", "eng_rating", 260.0, "kW"], + ["ac|propulsion|propeller|diameter", "prop_diameter", 2.5, "m"], + ["ac|propulsion|motor|rating", "motor_rating", 240.0, "kW"], + ["ac|propulsion|generator|rating", "gen_rating", 250.0, "kW"], + ["ac|weights|W_battery", "batt_weight", 2000, "kg"], + ] + self.add_subsystem("dvs", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) # introduce model components - self.add_subsystem('motor1', SimpleMotor(efficiency=0.97, num_nodes=nn)) - self.add_subsystem('prop1', SimplePropeller(num_nodes=nn), - promotes_inputs=["fltcond|*"], promotes_outputs=['thrust']) - self.connect('motor1.shaft_power_out', 'prop1.shaft_power_in') + self.add_subsystem("motor1", SimpleMotor(efficiency=0.97, num_nodes=nn)) + self.add_subsystem( + "prop1", SimplePropeller(num_nodes=nn), promotes_inputs=["fltcond|*"], promotes_outputs=["thrust"] + ) + self.connect("motor1.shaft_power_out", "prop1.shaft_power_in") - self.add_subsystem('hybrid_split', PowerSplit(rule='fraction', num_nodes=nn)) - self.connect('motor1.elec_load', 'hybrid_split.power_in') + self.add_subsystem("hybrid_split", PowerSplit(rule="fraction", num_nodes=nn)) + self.connect("motor1.elec_load", "hybrid_split.power_in") - self.add_subsystem('eng1', - SimpleTurboshaft(num_nodes=nn, - weight_inc=0.14 / 1000, - weight_base=104), - promotes_outputs=["fuel_flow"]) - self.add_subsystem('gen1',SimpleGenerator(efficiency=0.97, num_nodes=nn)) + self.add_subsystem( + "eng1", + SimpleTurboshaft(num_nodes=nn, weight_inc=0.14 / 1000, weight_base=104), + promotes_outputs=["fuel_flow"], + ) + self.add_subsystem("gen1", SimpleGenerator(efficiency=0.97, num_nodes=nn)) - self.connect('eng1.shaft_power_out', 'gen1.shaft_power_in') + self.connect("eng1.shaft_power_out", "gen1.shaft_power_in") - self.add_subsystem('batt1', SimpleBattery(num_nodes=nn, specific_energy=e_b)) - self.connect('hybrid_split.power_out_A', 'batt1.elec_load') + self.add_subsystem("batt1", SimpleBattery(num_nodes=nn, specific_energy=e_b)) + self.connect("hybrid_split.power_out_A", "batt1.elec_load") # need to use the optimizer to drive hybrid_split.power_out_B to the # same value as gen1.elec_power_out. # create a residual equation for power in vs power out from the generator - self.add_subsystem('eng_gen_resid', - AddSubtractComp(output_name='eng_gen_residual', - input_names=['gen_power_available', 'gen_power_required'], - vec_size=nn, units='kW', - scaling_factors=[1, -1])) - self.connect('hybrid_split.power_out_B', 'eng_gen_resid.gen_power_required') - self.connect('gen1.elec_power_out', 'eng_gen_resid.gen_power_available') + self.add_subsystem( + "eng_gen_resid", + AddSubtractComp( + output_name="eng_gen_residual", + input_names=["gen_power_available", "gen_power_required"], + vec_size=nn, + units="kW", + scaling_factors=[1, -1], + ), + ) + self.connect("hybrid_split.power_out_B", "eng_gen_resid.gen_power_required") + self.connect("gen1.elec_power_out", "eng_gen_resid.gen_power_available") # add the weights of all the motors and props # (forward-compatibility for twin series hybrid layout) - addweights = AddSubtractComp(output_name='motors_weight', - input_names=['motor1_weight'], - units='kg') - addweights.add_equation(output_name='propellers_weight', - input_names=['prop1_weight'], - units='kg') - self.add_subsystem('add_weights', subsys=addweights, - promotes_inputs=['*'],promotes_outputs=['*']) - - self.connect('motor1.component_weight', 'motor1_weight') - self.connect('prop1.component_weight', 'prop1_weight') - - #connect design variables to model component inputs - self.connect('eng_rating', 'eng1.shaft_power_rating') - self.connect('prop_diameter', ['prop1.diameter']) - self.connect('motor_rating', ['motor1.elec_power_rating']) - self.connect('motor_rating', ['prop1.power_rating']) - self.connect('gen_rating', 'gen1.elec_power_rating') - self.connect('batt_weight', 'batt1.battery_weight') + addweights = AddSubtractComp(output_name="motors_weight", input_names=["motor1_weight"], units="kg") + addweights.add_equation(output_name="propellers_weight", input_names=["prop1_weight"], units="kg") + self.add_subsystem("add_weights", subsys=addweights, promotes_inputs=["*"], promotes_outputs=["*"]) + + self.connect("motor1.component_weight", "motor1_weight") + self.connect("prop1.component_weight", "prop1_weight") + + # connect design variables to model component inputs + self.connect("eng_rating", "eng1.shaft_power_rating") + self.connect("prop_diameter", ["prop1.diameter"]) + self.connect("motor_rating", ["motor1.elec_power_rating"]) + self.connect("motor_rating", ["prop1.power_rating"]) + self.connect("gen_rating", "gen1.elec_power_rating") + self.connect("batt_weight", "batt1.battery_weight") class VehicleSizingModel(Group): def setup(self): - dvs = self.add_subsystem('dvs',IndepVarComp(),promotes_outputs=["*"]) - climb = self.add_subsystem('missionanalysis',MissionAnalysis(),promotes_inputs=["dv_*"]) - dvs.add_output('dv_prop1_diameter',3.0, units='m') - dvs.add_output('dv_motor1_rating',1.5, units='MW') - dvs.add_output('dv_gen1_rating',1.55, units='MW') - dvs.add_output('ac|propulsion|engine|rating',1.6, units='MW') - dvs.add_output('dv_batt1_weight',2000, units='kg') - + dvs = self.add_subsystem("dvs", IndepVarComp(), promotes_outputs=["*"]) + climb = self.add_subsystem("missionanalysis", MissionAnalysis(), promotes_inputs=["dv_*"]) + dvs.add_output("dv_prop1_diameter", 3.0, units="m") + dvs.add_output("dv_motor1_rating", 1.5, units="MW") + dvs.add_output("dv_gen1_rating", 1.55, units="MW") + dvs.add_output("ac|propulsion|engine|rating", 1.6, units="MW") + dvs.add_output("dv_batt1_weight", 2000, units="kg") if __name__ == "__main__": @@ -323,17 +354,17 @@ def setup(self): prob = Problem() - prob.model= VehicleSizingModel() + prob.model = VehicleSizingModel() prob.setup() prob.run_model() # print "------Prop 1-------" - print('Thrust: ' + str(prob['missionanalysis.propmodel.prop1.thrust'])) - plt.plot(prob['missionanalysis.propmodel.prop1.thrust']) + print("Thrust: " + str(prob["missionanalysis.propmodel.prop1.thrust"])) + plt.plot(prob["missionanalysis.propmodel.prop1.thrust"]) plt.show() - print('Weight: ' + str(prob['missionanalysis.propmodel.prop1.component_weight'])) + print("Weight: " + str(prob["missionanalysis.propmodel.prop1.component_weight"])) # print'Prop eff: ' + str(prob['prop1.eta_prop']) @@ -342,12 +373,10 @@ def setup(self): # print 'Elec load: ' + str(prob['motor1.elec_load']) # print 'Heat: ' + str(prob['motor1.heat_out']) - # print "------Battery-------" # print 'Elec load: ' + str(prob['batt1.elec_load']) # print 'Heat: ' + str(prob['batt1.heat_out']) - # print "------Generator-------" # print 'Shaft power: ' + str(prob['gen1.shaft_power_in']) # print 'Elec load: ' + str(prob['gen1.elec_power_out']) @@ -358,7 +387,7 @@ def setup(self): # print 'Shaft power: ' + str(prob['eng1.shaft_power_out']) # print 'Fuel flow:' + str(prob['eng1.fuel_flow']*60*60) - #prob.model.list_inputs() - #prob.model.list_outputs() - #prob.check_partials(compact_print=True) - #prob.check_totals(compact_print=True) + # prob.model.list_inputs() + # prob.model.list_outputs() + # prob.check_partials(compact_print=True) + # prob.check_totals(compact_print=True) diff --git a/openconcept/propulsion/systems/simple_turboprop.py b/openconcept/propulsion/systems/simple_turboprop.py index 9d34e247..04c5a7f4 100644 --- a/openconcept/propulsion/systems/simple_turboprop.py +++ b/openconcept/propulsion/systems/simple_turboprop.py @@ -2,6 +2,7 @@ from openconcept.utilities import DVLabel, AddSubtractComp, ElementMultiplyDivideComp from openmdao.api import Group + class TurbopropPropulsionSystem(Group): """ This is an example model of the simplest possible propulsion system @@ -22,7 +23,7 @@ class TurbopropPropulsionSystem(Group): Air density (vector, kg/m**3) fltcond|Utrue : float True airspeed (vector, m/s) - + Outputs ------- thrust : float @@ -35,35 +36,44 @@ class TurbopropPropulsionSystem(Group): num_nodes : float Number of analysis points to run (default 1) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] # rename incoming design variables - dvlist = [['ac|propulsion|engine|rating', 'eng1_rating', 850, 'hp'], - ['ac|propulsion|propeller|diameter', 'prop1_diameter', 2.3, 'm']] - self.add_subsystem('dvs', DVLabel(dvlist), - promotes_inputs=["*"], promotes_outputs=["*"]) + dvlist = [ + ["ac|propulsion|engine|rating", "eng1_rating", 850, "hp"], + ["ac|propulsion|propeller|diameter", "prop1_diameter", 2.3, "m"], + ] + self.add_subsystem("dvs", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) # introduce model components - self.add_subsystem('eng1', - SimpleTurboshaft(num_nodes=nn, weight_inc=0.14 / 1000, weight_base=104), - promotes_inputs=["throttle", ("shaft_power_rating", "ac|propulsion|engine|rating")], promotes_outputs=["fuel_flow"]) - self.add_subsystem('prop1', - SimplePropeller(num_nodes=nn, num_blades=4, - design_J=2.2, design_cp=0.55), - promotes_inputs=["fltcond|*", ("power_rating", "ac|propulsion|engine|rating"), - ("diameter", "ac|propulsion|propeller|diameter")], - promotes_outputs=["thrust"]) + self.add_subsystem( + "eng1", + SimpleTurboshaft(num_nodes=nn, weight_inc=0.14 / 1000, weight_base=104), + promotes_inputs=["throttle", ("shaft_power_rating", "ac|propulsion|engine|rating")], + promotes_outputs=["fuel_flow"], + ) + self.add_subsystem( + "prop1", + SimplePropeller(num_nodes=nn, num_blades=4, design_J=2.2, design_cp=0.55), + promotes_inputs=[ + "fltcond|*", + ("power_rating", "ac|propulsion|engine|rating"), + ("diameter", "ac|propulsion|propeller|diameter"), + ], + promotes_outputs=["thrust"], + ) # Set default values for the engine rating and prop diameter - self.set_input_defaults("ac|propulsion|engine|rating", 850., units="hp") + self.set_input_defaults("ac|propulsion|engine|rating", 850.0, units="hp") self.set_input_defaults("ac|propulsion|propeller|diameter", 2.3, units="m") # Connect shaft power from turboshaft to propeller - self.connect('eng1.shaft_power_out', 'prop1.shaft_power_in') + self.connect("eng1.shaft_power_out", "prop1.shaft_power_in") class TwinTurbopropPropulsionSystem(Group): @@ -99,44 +109,70 @@ class TwinTurbopropPropulsionSystem(Group): num_nodes : float Number of analysis points to run (default 1) """ + def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] # Introduce turboshaft and propeller components (one for each side) - self.add_subsystem('eng1',SimpleTurboshaft(num_nodes=nn,weight_inc=0.14/1000,weight_base=104),promotes_inputs=['throttle', ("shaft_power_rating", "ac|propulsion|engine|rating")]) - self.add_subsystem('prop1',SimplePropeller(num_nodes=nn,num_blades=4,design_J=2.2,design_cp=0.55),promotes_inputs=["fltcond|*", ("power_rating", "ac|propulsion|engine|rating"), ("diameter", "ac|propulsion|propeller|diameter")]) - self.add_subsystem('eng2',SimpleTurboshaft(num_nodes=nn,weight_inc=0.14/1000,weight_base=104),promotes_inputs=[("shaft_power_rating", "ac|propulsion|engine|rating")]) - self.add_subsystem('prop2',SimplePropeller(num_nodes=nn,num_blades=4,design_J=2.2,design_cp=0.55),promotes_inputs=["fltcond|*", ("power_rating", "ac|propulsion|engine|rating"), ("diameter", "ac|propulsion|propeller|diameter")]) + self.add_subsystem( + "eng1", + SimpleTurboshaft(num_nodes=nn, weight_inc=0.14 / 1000, weight_base=104), + promotes_inputs=["throttle", ("shaft_power_rating", "ac|propulsion|engine|rating")], + ) + self.add_subsystem( + "prop1", + SimplePropeller(num_nodes=nn, num_blades=4, design_J=2.2, design_cp=0.55), + promotes_inputs=[ + "fltcond|*", + ("power_rating", "ac|propulsion|engine|rating"), + ("diameter", "ac|propulsion|propeller|diameter"), + ], + ) + self.add_subsystem( + "eng2", + SimpleTurboshaft(num_nodes=nn, weight_inc=0.14 / 1000, weight_base=104), + promotes_inputs=[("shaft_power_rating", "ac|propulsion|engine|rating")], + ) + self.add_subsystem( + "prop2", + SimplePropeller(num_nodes=nn, num_blades=4, design_J=2.2, design_cp=0.55), + promotes_inputs=[ + "fltcond|*", + ("power_rating", "ac|propulsion|engine|rating"), + ("diameter", "ac|propulsion|propeller|diameter"), + ], + ) # Set default values for the engine rating and prop diameter - self.set_input_defaults("ac|propulsion|engine|rating", 750., units="hp") + self.set_input_defaults("ac|propulsion|engine|rating", 750.0, units="hp") self.set_input_defaults("ac|propulsion|propeller|diameter", 2.28, units="m") # Propulsion models expect a high-level 'throttle' parameter and a 'propulsor_active' flag to set individual throttles - failedengine = ElementMultiplyDivideComp() - failedengine.add_equation('eng2throttle',input_names=['throttle','propulsor_active'],vec_size=nn) - self.add_subsystem('failedengine', failedengine, - promotes_inputs=['throttle', 'propulsor_active']) - self.connect('failedengine.eng2throttle','eng2.throttle') + failedengine = ElementMultiplyDivideComp() + failedengine.add_equation("eng2throttle", input_names=["throttle", "propulsor_active"], vec_size=nn) + self.add_subsystem("failedengine", failedengine, promotes_inputs=["throttle", "propulsor_active"]) + self.connect("failedengine.eng2throttle", "eng2.throttle") # Connect components to each other - self.connect('eng1.shaft_power_out','prop1.shaft_power_in') - self.connect('eng2.shaft_power_out','prop2.shaft_power_in') + self.connect("eng1.shaft_power_out", "prop1.shaft_power_in") + self.connect("eng2.shaft_power_out", "prop2.shaft_power_in") # Add up the weights, thrusts and fuel flows - add1 = AddSubtractComp(output_name='fuel_flow',input_names=['eng1_fuel_flow','eng2_fuel_flow'],vec_size=nn, units='kg/s') - add1.add_equation(output_name='thrust',input_names=['prop1_thrust','prop2_thrust'],vec_size=nn, units='N') - add1.add_equation(output_name='engines_weight',input_names=['eng1_weight','eng2_weight'], units='kg') - add1.add_equation(output_name='propellers_weight',input_names=['prop1_weight','prop2_weight'], units='kg') - self.add_subsystem('adder',subsys=add1,promotes_inputs=["*"],promotes_outputs=["*"]) - self.connect('prop1.thrust','prop1_thrust') - self.connect('prop2.thrust','prop2_thrust') - self.connect('eng1.fuel_flow','eng1_fuel_flow') - self.connect('eng2.fuel_flow','eng2_fuel_flow') - self.connect('prop1.component_weight','prop1_weight') - self.connect('prop2.component_weight','prop2_weight') - self.connect('eng1.component_weight','eng1_weight') - self.connect('eng2.component_weight','eng2_weight') + add1 = AddSubtractComp( + output_name="fuel_flow", input_names=["eng1_fuel_flow", "eng2_fuel_flow"], vec_size=nn, units="kg/s" + ) + add1.add_equation(output_name="thrust", input_names=["prop1_thrust", "prop2_thrust"], vec_size=nn, units="N") + add1.add_equation(output_name="engines_weight", input_names=["eng1_weight", "eng2_weight"], units="kg") + add1.add_equation(output_name="propellers_weight", input_names=["prop1_weight", "prop2_weight"], units="kg") + self.add_subsystem("adder", subsys=add1, promotes_inputs=["*"], promotes_outputs=["*"]) + self.connect("prop1.thrust", "prop1_thrust") + self.connect("prop2.thrust", "prop2_thrust") + self.connect("eng1.fuel_flow", "eng1_fuel_flow") + self.connect("eng2.fuel_flow", "eng2_fuel_flow") + self.connect("prop1.component_weight", "prop1_weight") + self.connect("prop2.component_weight", "prop2_weight") + self.connect("eng1.component_weight", "eng1_weight") + self.connect("eng2.component_weight", "eng2_weight") diff --git a/openconcept/propulsion/systems/thermal_series_hybrid.py b/openconcept/propulsion/systems/thermal_series_hybrid.py index eb8b6cbb..97111196 100644 --- a/openconcept/propulsion/systems/thermal_series_hybrid.py +++ b/openconcept/propulsion/systems/thermal_series_hybrid.py @@ -1,5 +1,5 @@ - from openconcept.propulsion import SimpleMotor, PowerSplit, SimpleGenerator, SimpleTurboshaft, SimplePropeller + # I had to move specific energy into a design variable to get this outer loop to work correctly from openconcept.energy_storage import SOCBattery from openconcept.utilities import DVLabel, AddSubtractComp, ElementMultiplyDivideComp @@ -14,6 +14,7 @@ from openmdao.api import Problem, Group, IndepVarComp, BalanceComp import numpy as np + class TwinSeriesHybridElectricThermalPropulsionSystem(Group): """ This is an example model of a series-hybrid propulsion system. One motor @@ -46,136 +47,159 @@ class TwinSeriesHybridElectricThermalPropulsionSystem(Group): fuel_flow (integrate over time) elec_load (integrate over time to obtain SOC) motor2.throttle """ - def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + def initialize(self): + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] print(nn) - #define design variables that are independent of flight condition or control states - dvlist = [['ac|propulsion|engine|rating','eng_rating',260.0,'kW'], - ['ac|propulsion|propeller|diameter','prop_diameter',2.5,'m'], - ['ac|propulsion|motor|rating','motor_rating',240.0,'kW'], - ['ac|propulsion|generator|rating','gen_rating',250.0,'kW'], - ['ac|weights|W_battery','batt_weight',2000,'kg'], - ['ac|propulsion|thermal|hx|mdot_coolant','mdot_coolant',0.1*np.ones((nn,)),'kg/s'], - ['ac|propulsion|thermal|hx|coolant_mass','coolant_mass',10.,'kg'], - ['ac|propulsion|thermal|hx|channel_width','channel_width',1.,'mm'], - ['ac|propulsion|thermal|hx|channel_height','channel_height',20.,'mm'], - ['ac|propulsion|thermal|hx|channel_length','channel_length',0.2,'m'], - ['ac|propulsion|thermal|hx|n_parallel','n_parallel',50,None], - # ['ac|propulsion|thermal|duct|area_nozzle','area_nozzle',58.*np.ones((nn,)),'inch**2'], - ['ac|propulsion|battery|specific_energy','specific_energy',300,'W*h/kg'] - ] - - self.add_subsystem('dvs',DVLabel(dvlist),promotes_inputs=["*"],promotes_outputs=["*"]) - #introduce model components - self.add_subsystem('motor1', SimpleMotor(efficiency=0.97,num_nodes=nn),promotes_inputs=['throttle']) - self.add_subsystem('prop1',SimplePropeller(num_nodes=nn),promotes_inputs=["fltcond|*"]) - self.connect('motor1.shaft_power_out','prop1.shaft_power_in') - - #propulsion models expect a high-level 'throttle' parameter and a 'propulsor_active' flag to set individual throttles - failedengine = ElementMultiplyDivideComp() - failedengine.add_equation('motor2throttle',input_names=['throttle','propulsor_active'],vec_size=nn) - self.add_subsystem('failedmotor', failedengine, - promotes_inputs=['throttle', 'propulsor_active']) - - self.add_subsystem('motor2', SimpleMotor(efficiency=0.97,num_nodes=nn)) - self.add_subsystem('prop2',SimplePropeller(num_nodes=nn),promotes_inputs=["fltcond|*"]) - self.connect('motor2.shaft_power_out','prop2.shaft_power_in') - self.connect('failedmotor.motor2throttle','motor2.throttle') - - - - addpower = AddSubtractComp(output_name='motors_elec_load',input_names=['motor1_elec_load','motor2_elec_load'], units='kW',vec_size=nn) - addpower.add_equation(output_name='thrust',input_names=['prop1_thrust','prop2_thrust'], units='N',vec_size=nn) - self.add_subsystem('add_power',subsys=addpower,promotes_outputs=['*']) - self.connect('motor1.elec_load','add_power.motor1_elec_load') - self.connect('motor2.elec_load','add_power.motor2_elec_load') - self.connect('prop1.thrust','add_power.prop1_thrust') - self.connect('prop2.thrust','add_power.prop2_thrust') - - self.add_subsystem('hybrid_split',PowerSplit(rule='fraction',num_nodes=nn)) - self.connect('motors_elec_load','hybrid_split.power_in') - - self.add_subsystem('eng1',SimpleTurboshaft(num_nodes=nn,weight_inc=0.14/1000,weight_base=104),promotes_outputs=["fuel_flow"]) - self.add_subsystem('gen1',SimpleGenerator(efficiency=0.97,num_nodes=nn)) - - self.connect('eng1.shaft_power_out','gen1.shaft_power_in') - - self.add_subsystem('batt1', SOCBattery(num_nodes=nn, efficiency=0.97),promotes_inputs=["duration",'specific_energy']) - self.connect('hybrid_split.power_out_A','batt1.elec_load') - self.add_subsystem('eng_throttle_set',BalanceComp(name='eng_throttle', val=np.ones((nn,))*0.5, units=None, eq_units='kW', rhs_name='gen_power_required',lhs_name='gen_power_available')) - self.connect('hybrid_split.power_out_B','eng_throttle_set.gen_power_required') - self.connect('gen1.elec_power_out','eng_throttle_set.gen_power_available') - self.connect('eng_throttle_set.eng_throttle','eng1.throttle') - - adder = AddSubtractComp(output_name='motors_weight',input_names=['motor1_weight','motor2_weight'], units='kg') - adder.add_equation(output_name='propellers_weight',input_names=['prop1_weight','prop2_weight'], units='kg') - adder.add_equation(output_name='motors_heat',input_names=['motor1_heat','motor2_heat'], vec_size=nn, units='W') - self.add_subsystem('adder',subsys=adder,promotes_inputs=['*'],promotes_outputs=['*']) - relabel = [['hybrid_split_A_in','battery_load',np.ones(nn)*260.0,'kW']] - self.add_subsystem('relabel',DVLabel(relabel),promotes_outputs=["battery_load"]) - self.connect('hybrid_split.power_out_A','relabel.hybrid_split_A_in') - - self.connect('motor1.component_weight','motor1_weight') - self.connect('motor2.component_weight','motor2_weight') - self.connect('prop1.component_weight','prop1_weight') - self.connect('prop2.component_weight','prop2_weight') - self.connect('motor1.heat_out','motor1_heat') - self.connect('motor2.heat_out','motor2_heat') - - #connect design variables to model component inputs - self.connect('eng_rating','eng1.shaft_power_rating') - self.connect('prop_diameter',['prop1.diameter','prop2.diameter']) - self.connect('motor_rating',['motor1.elec_power_rating','motor2.elec_power_rating']) - self.connect('motor_rating',['prop1.power_rating','prop2.power_rating']) - self.connect('gen_rating','gen1.elec_power_rating') - self.connect('batt_weight','batt1.battery_weight') - iv = self.add_subsystem('iv',IndepVarComp(), promotes_outputs=['*']) - iv.add_output('rho_coolant', val=997*np.ones((nn,)),units='kg/m**3') - lc_promotes = ['duration','channel_*','n_parallel'] - - self.add_subsystem('batteryheatsink', - LiquidCooledComp(num_nodes=nn, - quasi_steady=False), - promotes_inputs=lc_promotes) - self.connect('batt1.heat_out','batteryheatsink.q_in') - self.connect('batt_weight','batteryheatsink.mass') - - self.add_subsystem('motorheatsink', - LiquidCooledComp(num_nodes=nn, - quasi_steady=False), - promotes_inputs=lc_promotes) - self.connect('motors_heat','motorheatsink.q_in') - self.connect('motors_weight','motorheatsink.mass') - - self.add_subsystem('duct', - ExplicitIncompressibleDuct(num_nodes=nn), - promotes_inputs=['fltcond|*']) - iv.add_output('ac|propulsion|thermal|duct|area_nozzle', val=58.*np.ones((nn,)), units='inch**2') - self.connect('ac|propulsion|thermal|duct|area_nozzle','duct.area_nozzle') - self.add_subsystem('hx',HXGroup(num_nodes=nn),promotes_inputs=['ac|*',('T_in_cold','fltcond|T'),('rho_cold','fltcond|rho')]) - self.connect('duct.mdot','hx.mdot_cold') - self.connect('hx.delta_p_cold','duct.delta_p_hex') - - self.connect('motorheatsink.T_out','hx.T_in_hot') - self.connect('rho_coolant','hx.rho_hot') - - self.add_subsystem('reservoir', - CoolantReservoir(num_nodes=nn), - promotes_inputs=['duration',('mass','coolant_mass')]) - self.connect('hx.T_out_hot','reservoir.T_in') - self.connect('reservoir.T_out','batteryheatsink.T_in') - self.connect('batteryheatsink.T_out','motorheatsink.T_in') - - self.connect('mdot_coolant',['batteryheatsink.mdot_coolant', - 'motorheatsink.mdot_coolant', - 'hx.mdot_hot', - 'reservoir.mdot_coolant']) + # define design variables that are independent of flight condition or control states + dvlist = [ + ["ac|propulsion|engine|rating", "eng_rating", 260.0, "kW"], + ["ac|propulsion|propeller|diameter", "prop_diameter", 2.5, "m"], + ["ac|propulsion|motor|rating", "motor_rating", 240.0, "kW"], + ["ac|propulsion|generator|rating", "gen_rating", 250.0, "kW"], + ["ac|weights|W_battery", "batt_weight", 2000, "kg"], + ["ac|propulsion|thermal|hx|mdot_coolant", "mdot_coolant", 0.1 * np.ones((nn,)), "kg/s"], + ["ac|propulsion|thermal|hx|coolant_mass", "coolant_mass", 10.0, "kg"], + ["ac|propulsion|thermal|hx|channel_width", "channel_width", 1.0, "mm"], + ["ac|propulsion|thermal|hx|channel_height", "channel_height", 20.0, "mm"], + ["ac|propulsion|thermal|hx|channel_length", "channel_length", 0.2, "m"], + ["ac|propulsion|thermal|hx|n_parallel", "n_parallel", 50, None], + # ['ac|propulsion|thermal|duct|area_nozzle','area_nozzle',58.*np.ones((nn,)),'inch**2'], + ["ac|propulsion|battery|specific_energy", "specific_energy", 300, "W*h/kg"], + ] + + self.add_subsystem("dvs", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) + # introduce model components + self.add_subsystem("motor1", SimpleMotor(efficiency=0.97, num_nodes=nn), promotes_inputs=["throttle"]) + self.add_subsystem("prop1", SimplePropeller(num_nodes=nn), promotes_inputs=["fltcond|*"]) + self.connect("motor1.shaft_power_out", "prop1.shaft_power_in") + + # propulsion models expect a high-level 'throttle' parameter and a 'propulsor_active' flag to set individual throttles + failedengine = ElementMultiplyDivideComp() + failedengine.add_equation("motor2throttle", input_names=["throttle", "propulsor_active"], vec_size=nn) + self.add_subsystem("failedmotor", failedengine, promotes_inputs=["throttle", "propulsor_active"]) + + self.add_subsystem("motor2", SimpleMotor(efficiency=0.97, num_nodes=nn)) + self.add_subsystem("prop2", SimplePropeller(num_nodes=nn), promotes_inputs=["fltcond|*"]) + self.connect("motor2.shaft_power_out", "prop2.shaft_power_in") + self.connect("failedmotor.motor2throttle", "motor2.throttle") + + addpower = AddSubtractComp( + output_name="motors_elec_load", + input_names=["motor1_elec_load", "motor2_elec_load"], + units="kW", + vec_size=nn, + ) + addpower.add_equation( + output_name="thrust", input_names=["prop1_thrust", "prop2_thrust"], units="N", vec_size=nn + ) + self.add_subsystem("add_power", subsys=addpower, promotes_outputs=["*"]) + self.connect("motor1.elec_load", "add_power.motor1_elec_load") + self.connect("motor2.elec_load", "add_power.motor2_elec_load") + self.connect("prop1.thrust", "add_power.prop1_thrust") + self.connect("prop2.thrust", "add_power.prop2_thrust") + + self.add_subsystem("hybrid_split", PowerSplit(rule="fraction", num_nodes=nn)) + self.connect("motors_elec_load", "hybrid_split.power_in") + + self.add_subsystem( + "eng1", + SimpleTurboshaft(num_nodes=nn, weight_inc=0.14 / 1000, weight_base=104), + promotes_outputs=["fuel_flow"], + ) + self.add_subsystem("gen1", SimpleGenerator(efficiency=0.97, num_nodes=nn)) + + self.connect("eng1.shaft_power_out", "gen1.shaft_power_in") + + self.add_subsystem( + "batt1", SOCBattery(num_nodes=nn, efficiency=0.97), promotes_inputs=["duration", "specific_energy"] + ) + self.connect("hybrid_split.power_out_A", "batt1.elec_load") + self.add_subsystem( + "eng_throttle_set", + BalanceComp( + name="eng_throttle", + val=np.ones((nn,)) * 0.5, + units=None, + eq_units="kW", + rhs_name="gen_power_required", + lhs_name="gen_power_available", + ), + ) + self.connect("hybrid_split.power_out_B", "eng_throttle_set.gen_power_required") + self.connect("gen1.elec_power_out", "eng_throttle_set.gen_power_available") + self.connect("eng_throttle_set.eng_throttle", "eng1.throttle") + + adder = AddSubtractComp(output_name="motors_weight", input_names=["motor1_weight", "motor2_weight"], units="kg") + adder.add_equation(output_name="propellers_weight", input_names=["prop1_weight", "prop2_weight"], units="kg") + adder.add_equation( + output_name="motors_heat", input_names=["motor1_heat", "motor2_heat"], vec_size=nn, units="W" + ) + self.add_subsystem("adder", subsys=adder, promotes_inputs=["*"], promotes_outputs=["*"]) + relabel = [["hybrid_split_A_in", "battery_load", np.ones(nn) * 260.0, "kW"]] + self.add_subsystem("relabel", DVLabel(relabel), promotes_outputs=["battery_load"]) + self.connect("hybrid_split.power_out_A", "relabel.hybrid_split_A_in") + + self.connect("motor1.component_weight", "motor1_weight") + self.connect("motor2.component_weight", "motor2_weight") + self.connect("prop1.component_weight", "prop1_weight") + self.connect("prop2.component_weight", "prop2_weight") + self.connect("motor1.heat_out", "motor1_heat") + self.connect("motor2.heat_out", "motor2_heat") + + # connect design variables to model component inputs + self.connect("eng_rating", "eng1.shaft_power_rating") + self.connect("prop_diameter", ["prop1.diameter", "prop2.diameter"]) + self.connect("motor_rating", ["motor1.elec_power_rating", "motor2.elec_power_rating"]) + self.connect("motor_rating", ["prop1.power_rating", "prop2.power_rating"]) + self.connect("gen_rating", "gen1.elec_power_rating") + self.connect("batt_weight", "batt1.battery_weight") + iv = self.add_subsystem("iv", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("rho_coolant", val=997 * np.ones((nn,)), units="kg/m**3") + lc_promotes = ["duration", "channel_*", "n_parallel"] + + self.add_subsystem( + "batteryheatsink", LiquidCooledComp(num_nodes=nn, quasi_steady=False), promotes_inputs=lc_promotes + ) + self.connect("batt1.heat_out", "batteryheatsink.q_in") + self.connect("batt_weight", "batteryheatsink.mass") + + self.add_subsystem( + "motorheatsink", LiquidCooledComp(num_nodes=nn, quasi_steady=False), promotes_inputs=lc_promotes + ) + self.connect("motors_heat", "motorheatsink.q_in") + self.connect("motors_weight", "motorheatsink.mass") + + self.add_subsystem("duct", ExplicitIncompressibleDuct(num_nodes=nn), promotes_inputs=["fltcond|*"]) + iv.add_output("ac|propulsion|thermal|duct|area_nozzle", val=58.0 * np.ones((nn,)), units="inch**2") + self.connect("ac|propulsion|thermal|duct|area_nozzle", "duct.area_nozzle") + self.add_subsystem( + "hx", + HXGroup(num_nodes=nn), + promotes_inputs=["ac|*", ("T_in_cold", "fltcond|T"), ("rho_cold", "fltcond|rho")], + ) + self.connect("duct.mdot", "hx.mdot_cold") + self.connect("hx.delta_p_cold", "duct.delta_p_hex") + + self.connect("motorheatsink.T_out", "hx.T_in_hot") + self.connect("rho_coolant", "hx.rho_hot") + + self.add_subsystem( + "reservoir", CoolantReservoir(num_nodes=nn), promotes_inputs=["duration", ("mass", "coolant_mass")] + ) + self.connect("hx.T_out_hot", "reservoir.T_in") + self.connect("reservoir.T_out", "batteryheatsink.T_in") + self.connect("batteryheatsink.T_out", "motorheatsink.T_in") + + self.connect( + "mdot_coolant", + ["batteryheatsink.mdot_coolant", "motorheatsink.mdot_coolant", "hx.mdot_hot", "reservoir.mdot_coolant"], + ) class TwinSeriesHybridElectricThermalPropulsionRefrigerated(Group): @@ -213,169 +237,189 @@ class TwinSeriesHybridElectricThermalPropulsionRefrigerated(Group): fuel_flow (integrate over time) elec_load (integrate over time to obtain SOC) motor2.throttle """ - def initialize(self): - self.options.declare('num_nodes',default=1,desc="Number of mission analysis points to run") + def initialize(self): + self.options.declare("num_nodes", default=1, desc="Number of mission analysis points to run") def setup(self): - nn = self.options['num_nodes'] - - #define design variables that are independent of flight condition or control states - dvlist = [['ac|propulsion|engine|rating','eng_rating',260.0,'kW'], - ['ac|propulsion|propeller|diameter','prop_diameter',2.5,'m'], - ['ac|propulsion|motor|rating','motor_rating',240.0,'kW'], - ['ac|propulsion|generator|rating','gen_rating',250.0,'kW'], - ['ac|weights|W_battery','batt_weight',2000,'kg'], - ['ac|propulsion|thermal|refrig|rating','refrig_rating',1.,'kW'], - ['ac|propulsion|thermal|refrig|specific_power','refrig_spec_pow',200.,'W/kg'], - ['ac|propulsion|thermal|refrig|eff','refrig_eff_factor',0.4,None], - ['ac|propulsion|thermal|hx|mdot_coolant','mdot_coolant',0.1*np.ones((nn,)),'kg/s'], - ['ac|propulsion|thermal|hx|coolant_mass','coolant_mass',10.,'kg'], - ['ac|propulsion|thermal|hx|channel_width','channel_width',1.,'mm'], - ['ac|propulsion|thermal|hx|channel_height','channel_height',20.,'mm'], - ['ac|propulsion|thermal|hx|channel_length','channel_length',0.2,'m'], - ['ac|propulsion|thermal|hx|n_parallel','n_parallel',50,None], - ['ac|propulsion|thermal|duct|area_nozzle','area_nozzle',58.*np.ones((nn,)),'inch**2'], - ['ac|propulsion|battery|specific_energy','specific_energy',300,'W*h/kg'] - ] - - self.add_subsystem('dvs',DVLabel(dvlist),promotes_inputs=["*"],promotes_outputs=["*"]) - #introduce model components - self.add_subsystem('motor1', SimpleMotor(efficiency=0.97,num_nodes=nn),promotes_inputs=['throttle']) - self.add_subsystem('prop1',SimplePropeller(num_nodes=nn),promotes_inputs=["fltcond|*"]) - self.connect('motor1.shaft_power_out','prop1.shaft_power_in') - - #propulsion models expect a high-level 'throttle' parameter and a 'propulsor_active' flag to set individual throttles - failedengine = ElementMultiplyDivideComp() - failedengine.add_equation('motor2throttle',input_names=['throttle','propulsor_active'],vec_size=nn) - self.add_subsystem('failedmotor', failedengine, - promotes_inputs=['throttle', 'propulsor_active']) - - self.add_subsystem('motor2', SimpleMotor(efficiency=0.97,num_nodes=nn)) - self.add_subsystem('prop2',SimplePropeller(num_nodes=nn),promotes_inputs=["fltcond|*"]) - self.connect('motor2.shaft_power_out','prop2.shaft_power_in') - self.connect('failedmotor.motor2throttle','motor2.throttle') - - addpower = AddSubtractComp(output_name='total_elec_load', - input_names=['motor1_elec_load','motor2_elec_load', 'refrig_elec_load'], units='kW',vec_size=nn) - addpower.add_equation(output_name='thrust',input_names=['prop1_thrust','prop2_thrust'], units='N',vec_size=nn) - self.add_subsystem('add_power',subsys=addpower,promotes_outputs=['*']) - self.connect('motor1.elec_load','add_power.motor1_elec_load') - self.connect('motor2.elec_load','add_power.motor2_elec_load') - self.connect('prop1.thrust','add_power.prop1_thrust') - self.connect('prop2.thrust','add_power.prop2_thrust') - - - self.add_subsystem('hybrid_split',PowerSplit(rule='fraction',num_nodes=nn)) - self.connect('total_elec_load','hybrid_split.power_in') - - self.add_subsystem('eng1',SimpleTurboshaft(num_nodes=nn,weight_inc=0.14/1000,weight_base=104),promotes_outputs=["fuel_flow"]) - self.add_subsystem('gen1',SimpleGenerator(efficiency=0.97,num_nodes=nn)) - - self.connect('eng1.shaft_power_out','gen1.shaft_power_in') - - self.add_subsystem('batt1', SOCBattery(num_nodes=nn, efficiency=0.97),promotes_inputs=["duration",'specific_energy']) - self.connect('hybrid_split.power_out_A','batt1.elec_load') + nn = self.options["num_nodes"] + + # define design variables that are independent of flight condition or control states + dvlist = [ + ["ac|propulsion|engine|rating", "eng_rating", 260.0, "kW"], + ["ac|propulsion|propeller|diameter", "prop_diameter", 2.5, "m"], + ["ac|propulsion|motor|rating", "motor_rating", 240.0, "kW"], + ["ac|propulsion|generator|rating", "gen_rating", 250.0, "kW"], + ["ac|weights|W_battery", "batt_weight", 2000, "kg"], + ["ac|propulsion|thermal|refrig|rating", "refrig_rating", 1.0, "kW"], + ["ac|propulsion|thermal|refrig|specific_power", "refrig_spec_pow", 200.0, "W/kg"], + ["ac|propulsion|thermal|refrig|eff", "refrig_eff_factor", 0.4, None], + ["ac|propulsion|thermal|hx|mdot_coolant", "mdot_coolant", 0.1 * np.ones((nn,)), "kg/s"], + ["ac|propulsion|thermal|hx|coolant_mass", "coolant_mass", 10.0, "kg"], + ["ac|propulsion|thermal|hx|channel_width", "channel_width", 1.0, "mm"], + ["ac|propulsion|thermal|hx|channel_height", "channel_height", 20.0, "mm"], + ["ac|propulsion|thermal|hx|channel_length", "channel_length", 0.2, "m"], + ["ac|propulsion|thermal|hx|n_parallel", "n_parallel", 50, None], + ["ac|propulsion|thermal|duct|area_nozzle", "area_nozzle", 58.0 * np.ones((nn,)), "inch**2"], + ["ac|propulsion|battery|specific_energy", "specific_energy", 300, "W*h/kg"], + ] + + self.add_subsystem("dvs", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) + # introduce model components + self.add_subsystem("motor1", SimpleMotor(efficiency=0.97, num_nodes=nn), promotes_inputs=["throttle"]) + self.add_subsystem("prop1", SimplePropeller(num_nodes=nn), promotes_inputs=["fltcond|*"]) + self.connect("motor1.shaft_power_out", "prop1.shaft_power_in") + + # propulsion models expect a high-level 'throttle' parameter and a 'propulsor_active' flag to set individual throttles + failedengine = ElementMultiplyDivideComp() + failedengine.add_equation("motor2throttle", input_names=["throttle", "propulsor_active"], vec_size=nn) + self.add_subsystem("failedmotor", failedengine, promotes_inputs=["throttle", "propulsor_active"]) + + self.add_subsystem("motor2", SimpleMotor(efficiency=0.97, num_nodes=nn)) + self.add_subsystem("prop2", SimplePropeller(num_nodes=nn), promotes_inputs=["fltcond|*"]) + self.connect("motor2.shaft_power_out", "prop2.shaft_power_in") + self.connect("failedmotor.motor2throttle", "motor2.throttle") + + addpower = AddSubtractComp( + output_name="total_elec_load", + input_names=["motor1_elec_load", "motor2_elec_load", "refrig_elec_load"], + units="kW", + vec_size=nn, + ) + addpower.add_equation( + output_name="thrust", input_names=["prop1_thrust", "prop2_thrust"], units="N", vec_size=nn + ) + self.add_subsystem("add_power", subsys=addpower, promotes_outputs=["*"]) + self.connect("motor1.elec_load", "add_power.motor1_elec_load") + self.connect("motor2.elec_load", "add_power.motor2_elec_load") + self.connect("prop1.thrust", "add_power.prop1_thrust") + self.connect("prop2.thrust", "add_power.prop2_thrust") + + self.add_subsystem("hybrid_split", PowerSplit(rule="fraction", num_nodes=nn)) + self.connect("total_elec_load", "hybrid_split.power_in") + + self.add_subsystem( + "eng1", + SimpleTurboshaft(num_nodes=nn, weight_inc=0.14 / 1000, weight_base=104), + promotes_outputs=["fuel_flow"], + ) + self.add_subsystem("gen1", SimpleGenerator(efficiency=0.97, num_nodes=nn)) + + self.connect("eng1.shaft_power_out", "gen1.shaft_power_in") + + self.add_subsystem( + "batt1", SOCBattery(num_nodes=nn, efficiency=0.97), promotes_inputs=["duration", "specific_energy"] + ) + self.connect("hybrid_split.power_out_A", "batt1.elec_load") # TODO set val= right number of nn - self.add_subsystem('eng_throttle_set',BalanceComp(name='eng_throttle', val=np.ones((nn,))*0.5, units=None, eq_units='kW', - rhs_name='gen_power_required',lhs_name='gen_power_available')) - #need to use the optimizer to drive hybrid_split.power_out_B to the same value as gen1.elec_power_out - self.connect('hybrid_split.power_out_B','eng_throttle_set.gen_power_required') - self.connect('gen1.elec_power_out','eng_throttle_set.gen_power_available') - self.connect('eng_throttle_set.eng_throttle','eng1.throttle') - - adder = AddSubtractComp(output_name='motors_weight',input_names=['motor1_weight','motor2_weight'], units='kg') - adder.add_equation(output_name='propellers_weight',input_names=['prop1_weight','prop2_weight'], units='kg') - adder.add_equation(output_name='motors_heat',input_names=['motor1_heat','motor2_heat'], vec_size=nn, units='W') - self.add_subsystem('adder',subsys=adder,promotes_inputs=['*'],promotes_outputs=['*']) - relabel = [['hybrid_split_A_in','battery_load',np.ones(nn)*260.0,'kW']] - self.add_subsystem('relabel',DVLabel(relabel),promotes_outputs=["battery_load"]) - self.connect('hybrid_split.power_out_A','relabel.hybrid_split_A_in') - - self.connect('motor1.component_weight','motor1_weight') - self.connect('motor2.component_weight','motor2_weight') - self.connect('prop1.component_weight','prop1_weight') - self.connect('prop2.component_weight','prop2_weight') - self.connect('motor1.heat_out','motor1_heat') - self.connect('motor2.heat_out','motor2_heat') - - #connect design variables to model component inputs - self.connect('eng_rating','eng1.shaft_power_rating') - self.connect('prop_diameter',['prop1.diameter','prop2.diameter']) - self.connect('motor_rating',['motor1.elec_power_rating','motor2.elec_power_rating']) - self.connect('motor_rating',['prop1.power_rating','prop2.power_rating']) - self.connect('gen_rating','gen1.elec_power_rating') - self.connect('batt_weight','batt1.battery_weight') - - iv = self.add_subsystem('iv',IndepVarComp(), promotes_outputs=['*']) - - rho_coolant = 997. # kg/m^3 - iv.add_output('rho_coolant', val=rho_coolant*np.ones((nn,)),units='kg/m**3') - lc_promotes = ['duration','channel_*','n_parallel'] + self.add_subsystem( + "eng_throttle_set", + BalanceComp( + name="eng_throttle", + val=np.ones((nn,)) * 0.5, + units=None, + eq_units="kW", + rhs_name="gen_power_required", + lhs_name="gen_power_available", + ), + ) + # need to use the optimizer to drive hybrid_split.power_out_B to the same value as gen1.elec_power_out + self.connect("hybrid_split.power_out_B", "eng_throttle_set.gen_power_required") + self.connect("gen1.elec_power_out", "eng_throttle_set.gen_power_available") + self.connect("eng_throttle_set.eng_throttle", "eng1.throttle") + + adder = AddSubtractComp(output_name="motors_weight", input_names=["motor1_weight", "motor2_weight"], units="kg") + adder.add_equation(output_name="propellers_weight", input_names=["prop1_weight", "prop2_weight"], units="kg") + adder.add_equation( + output_name="motors_heat", input_names=["motor1_heat", "motor2_heat"], vec_size=nn, units="W" + ) + self.add_subsystem("adder", subsys=adder, promotes_inputs=["*"], promotes_outputs=["*"]) + relabel = [["hybrid_split_A_in", "battery_load", np.ones(nn) * 260.0, "kW"]] + self.add_subsystem("relabel", DVLabel(relabel), promotes_outputs=["battery_load"]) + self.connect("hybrid_split.power_out_A", "relabel.hybrid_split_A_in") + + self.connect("motor1.component_weight", "motor1_weight") + self.connect("motor2.component_weight", "motor2_weight") + self.connect("prop1.component_weight", "prop1_weight") + self.connect("prop2.component_weight", "prop2_weight") + self.connect("motor1.heat_out", "motor1_heat") + self.connect("motor2.heat_out", "motor2_heat") + + # connect design variables to model component inputs + self.connect("eng_rating", "eng1.shaft_power_rating") + self.connect("prop_diameter", ["prop1.diameter", "prop2.diameter"]) + self.connect("motor_rating", ["motor1.elec_power_rating", "motor2.elec_power_rating"]) + self.connect("motor_rating", ["prop1.power_rating", "prop2.power_rating"]) + self.connect("gen_rating", "gen1.elec_power_rating") + self.connect("batt_weight", "batt1.battery_weight") + + iv = self.add_subsystem("iv", IndepVarComp(), promotes_outputs=["*"]) + + rho_coolant = 997.0 # kg/m^3 + iv.add_output("rho_coolant", val=rho_coolant * np.ones((nn,)), units="kg/m**3") + lc_promotes = ["duration", "channel_*", "n_parallel"] # Add the refrigerator's electrical load to the splitter with the two motors # so it pulls power from both the battery and turboshaft at the hybridization ratio. # Bypass the refrigeration with refrig.control.bypass_start and refrig.control.bypass_end - self.add_subsystem('refrig', HeatPumpWithIntegratedCoolantLoop(num_nodes=nn)) - self.connect('refrig.elec_load', 'add_power.refrig_elec_load') - self.connect('refrig_eff_factor', 'refrig.eff_factor') - self.connect('refrig_rating', 'refrig.power_rating') - self.connect('refrig_spec_pow', 'refrig.specific_power') + self.add_subsystem("refrig", HeatPumpWithIntegratedCoolantLoop(num_nodes=nn)) + self.connect("refrig.elec_load", "add_power.refrig_elec_load") + self.connect("refrig_eff_factor", "refrig.eff_factor") + self.connect("refrig_rating", "refrig.power_rating") + self.connect("refrig_spec_pow", "refrig.specific_power") # Coolant loop on electrical component side (cooling side of refrigerator) # ,---> battery ---> motor ---, # | | # '---- refrig cold side <----' - self.add_subsystem('batteryheatsink', - LiquidCooledComp(num_nodes=nn, - quasi_steady=False), - promotes_inputs=lc_promotes) - self.connect('batt1.heat_out','batteryheatsink.q_in') - self.connect('batt_weight','batteryheatsink.mass') - self.connect('refrig.T_out_cold', 'batteryheatsink.T_in') - - self.add_subsystem('motorheatsink', - LiquidCooledComp(num_nodes=nn, - quasi_steady=False), - promotes_inputs=lc_promotes) - self.connect('motors_heat','motorheatsink.q_in') - self.connect('motors_weight','motorheatsink.mass') - self.connect('motorheatsink.T_out', 'refrig.T_in_cold') - self.connect('batteryheatsink.T_out', 'motorheatsink.T_in') - - self.connect('mdot_coolant',['batteryheatsink.mdot_coolant', - 'motorheatsink.mdot_coolant', - 'refrig.mdot_coolant']) - + self.add_subsystem( + "batteryheatsink", LiquidCooledComp(num_nodes=nn, quasi_steady=False), promotes_inputs=lc_promotes + ) + self.connect("batt1.heat_out", "batteryheatsink.q_in") + self.connect("batt_weight", "batteryheatsink.mass") + self.connect("refrig.T_out_cold", "batteryheatsink.T_in") + + self.add_subsystem( + "motorheatsink", LiquidCooledComp(num_nodes=nn, quasi_steady=False), promotes_inputs=lc_promotes + ) + self.connect("motors_heat", "motorheatsink.q_in") + self.connect("motors_weight", "motorheatsink.mass") + self.connect("motorheatsink.T_out", "refrig.T_in_cold") + self.connect("batteryheatsink.T_out", "motorheatsink.T_in") + + self.connect( + "mdot_coolant", ["batteryheatsink.mdot_coolant", "motorheatsink.mdot_coolant", "refrig.mdot_coolant"] + ) # Coolant loop on hot side of refrigerator to reject heat # ,----> refrigerator hot side -----, # | | # '----- heat exchanger/duct <------' - self.add_subsystem('duct', - ExplicitIncompressibleDuct(num_nodes=nn), - promotes_inputs=['fltcond|*']) - self.add_subsystem('hx',HXGroup(num_nodes=nn),promotes_inputs=['ac|*',('rho_cold','fltcond|rho'),('T_in_cold','fltcond|T')]) - self.connect('duct.mdot','hx.mdot_cold') - self.connect('hx.delta_p_cold','duct.delta_p_hex') + self.add_subsystem("duct", ExplicitIncompressibleDuct(num_nodes=nn), promotes_inputs=["fltcond|*"]) + self.add_subsystem( + "hx", + HXGroup(num_nodes=nn), + promotes_inputs=["ac|*", ("rho_cold", "fltcond|rho"), ("T_in_cold", "fltcond|T")], + ) + self.connect("duct.mdot", "hx.mdot_cold") + self.connect("hx.delta_p_cold", "duct.delta_p_hex") - self.connect('rho_coolant','hx.rho_hot') - self.connect('refrig.T_out_hot','hx.T_in_hot') - self.connect('hx.T_out_hot','refrig.T_in_hot') + self.connect("rho_coolant", "hx.rho_hot") + self.connect("refrig.T_out_hot", "hx.T_in_hot") + self.connect("hx.T_out_hot", "refrig.T_in_hot") - self.connect('mdot_coolant', 'hx.mdot_hot') + self.connect("mdot_coolant", "hx.mdot_hot") class VehicleSizingModel(Group): def setup(self): - dvs = self.add_subsystem('dvs',IndepVarComp(),promotes_outputs=["*"]) - climb = self.add_subsystem('missionanalysis',MissionAnalysis(),promotes_inputs=["dv_*"]) - dvs.add_output('dv_prop1_diameter',3.0, units='m') - dvs.add_output('dv_motor1_rating',1.5, units='MW') - dvs.add_output('dv_gen1_rating',1.55, units='MW') - dvs.add_output('ac|propulsion|engine|rating',1.6, units='MW') - dvs.add_output('dv_batt1_weight',2000, units='kg') - + dvs = self.add_subsystem("dvs", IndepVarComp(), promotes_outputs=["*"]) + climb = self.add_subsystem("missionanalysis", MissionAnalysis(), promotes_inputs=["dv_*"]) + dvs.add_output("dv_prop1_diameter", 3.0, units="m") + dvs.add_output("dv_motor1_rating", 1.5, units="MW") + dvs.add_output("dv_gen1_rating", 1.55, units="MW") + dvs.add_output("ac|propulsion|engine|rating", 1.6, units="MW") + dvs.add_output("dv_batt1_weight", 2000, units="kg") if __name__ == "__main__": @@ -383,17 +427,17 @@ def setup(self): prob = Problem() - prob.model= VehicleSizingModel() + prob.model = VehicleSizingModel() prob.setup() prob.run_model() # print "------Prop 1-------" - print('Thrust: ' + str(prob['missionanalysis.propmodel.prop1.thrust'])) - plt.plot(prob['missionanalysis.propmodel.prop1.thrust']) + print("Thrust: " + str(prob["missionanalysis.propmodel.prop1.thrust"])) + plt.plot(prob["missionanalysis.propmodel.prop1.thrust"]) plt.show() - print('Weight: ' + str(prob['missionanalysis.propmodel.prop1.component_weight'])) + print("Weight: " + str(prob["missionanalysis.propmodel.prop1.component_weight"])) # print'Prop eff: ' + str(prob['prop1.eta_prop']) @@ -402,12 +446,10 @@ def setup(self): # print 'Elec load: ' + str(prob['motor1.elec_load']) # print 'Heat: ' + str(prob['motor1.heat_out']) - # print "------Battery-------" # print 'Elec load: ' + str(prob['batt1.elec_load']) # print 'Heat: ' + str(prob['batt1.heat_out']) - # print "------Generator-------" # print 'Shaft power: ' + str(prob['gen1.shaft_power_in']) # print 'Elec load: ' + str(prob['gen1.elec_power_out']) @@ -418,7 +460,7 @@ def setup(self): # print 'Shaft power: ' + str(prob['eng1.shaft_power_out']) # print 'Fuel flow:' + str(prob['eng1.fuel_flow']*60*60) - #prob.model.list_inputs() - #prob.model.list_outputs() - #prob.check_partials(compact_print=True) - #prob.check_totals(compact_print=True) + # prob.model.list_inputs() + # prob.model.list_outputs() + # prob.check_partials(compact_print=True) + # prob.check_totals(compact_print=True) diff --git a/openconcept/propulsion/tests/test_N3.py b/openconcept/propulsion/tests/test_N3.py index 4947f01a..64603016 100644 --- a/openconcept/propulsion/tests/test_N3.py +++ b/openconcept/propulsion/tests/test_N3.py @@ -8,24 +8,28 @@ # Skip these test cases if the cached surrogate files don't exist # N+3 hybrid -hybrid_file_root = openconcept.__path__[0] + r'/propulsion/empirical_data/n+3_hybrid/' -hybrid_cached_thrust = os.path.exists(hybrid_file_root + r'/n3_hybrid_thrust_trained.zip') -hybrid_cached_fuelburn = os.path.exists(hybrid_file_root + r'n3_hybrid_fuelflow_trained.zip') -hybrid_cached_surge = os.path.exists(hybrid_file_root + 'n3_hybrid_smw_trained.zip') +hybrid_file_root = openconcept.__path__[0] + r"/propulsion/empirical_data/n+3_hybrid/" +hybrid_cached_thrust = os.path.exists(hybrid_file_root + r"/n3_hybrid_thrust_trained.zip") +hybrid_cached_fuelburn = os.path.exists(hybrid_file_root + r"n3_hybrid_fuelflow_trained.zip") +hybrid_cached_surge = os.path.exists(hybrid_file_root + "n3_hybrid_smw_trained.zip") hybrid_skip_tests = True if hybrid_cached_thrust and hybrid_cached_fuelburn and hybrid_cached_surge: hybrid_skip_tests = False # N+3 -file_root = openconcept.__path__[0] + r'/propulsion/empirical_data/n+3/' -cached_thrust = os.path.exists(file_root + r'/n3_thrust_trained.zip') -cached_fuelburn = os.path.exists(file_root + r'n3_fuelflow_trained.zip') -cached_T4 = os.path.exists(file_root + r'n3_smw_trained.zip') +file_root = openconcept.__path__[0] + r"/propulsion/empirical_data/n+3/" +cached_thrust = os.path.exists(file_root + r"/n3_thrust_trained.zip") +cached_fuelburn = os.path.exists(file_root + r"n3_fuelflow_trained.zip") +cached_T4 = os.path.exists(file_root + r"n3_smw_trained.zip") skip_tests = True if cached_thrust and cached_fuelburn and cached_T4: skip_tests = False -@unittest.skipIf(hybrid_skip_tests, "N+3 hybrid surrogate model has not been trained (cached data not found), so skipping N+3 hybrid tests") + +@unittest.skipIf( + hybrid_skip_tests, + "N+3 hybrid surrogate model has not been trained (cached data not found), so skipping N+3 hybrid tests", +) class N3HybridTestCase(unittest.TestCase): def test_defaults(self): p = Problem() @@ -33,18 +37,18 @@ def test_defaults(self): p.setup(force_alloc_complex=True) - p.set_val('throttle', 0.5) - p.set_val('fltcond|h', 10e3, units='ft') - p.set_val('fltcond|M', 0.5) - p.set_val('hybrid_power', 200., units='kW') + p.set_val("throttle", 0.5) + p.set_val("fltcond|h", 10e3, units="ft") + p.set_val("fltcond|M", 0.5) + p.set_val("hybrid_power", 200.0, units="kW") p.run_model() - assert_near_equal(p.get_val('thrust', units='lbf'), 6965.43674107*np.ones(1), tolerance=1e-6) - assert_near_equal(p.get_val('fuel_flow', units='kg/s'), 0.34333925*np.ones(1), tolerance=1e-6) - assert_near_equal(p.get_val('surge_margin'), 17.49872296*np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("thrust", units="lbf"), 6965.43674107 * np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("fuel_flow", units="kg/s"), 0.34333925 * np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("surge_margin"), 17.49872296 * np.ones(1), tolerance=1e-6) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_vectorized(self): @@ -54,23 +58,33 @@ def test_vectorized(self): p.setup(force_alloc_complex=True) - p.set_val('throttle', np.linspace(0.0001, 1., nn)) - p.set_val('fltcond|h', np.linspace(0, 40e3, nn), units='ft') - p.set_val('fltcond|M', np.linspace(0.1, 0.9, nn)) - p.set_val('hybrid_power', np.linspace(1e3, 0, nn), units='kW') + p.set_val("throttle", np.linspace(0.0001, 1.0, nn)) + p.set_val("fltcond|h", np.linspace(0, 40e3, nn), units="ft") + p.set_val("fltcond|M", np.linspace(0.1, 0.9, nn)) + p.set_val("hybrid_power", np.linspace(1e3, 0, nn), units="kW") p.run_model() - assert_near_equal(p.get_val('thrust', units='lbf'), np.array([2143.74837065, 4298.37044104, - 5731.73572266, 5567.1704698, 6093.64182948]), tolerance=5e-3) - assert_near_equal(p.get_val('fuel_flow', units='kg/s'), np.array([0.12830921, - 0.15107399, 0.24857052, 0.28013556, 0.24271405]), tolerance=5e-3) - assert_near_equal(p.get_val('surge_margin'), np.array([3.62385489, 5.13891739, 17.61488138, - 29.65131358, 17.48630861]), tolerance=5e-3) - - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal( + p.get_val("thrust", units="lbf"), + np.array([2143.74837065, 4298.37044104, 5731.73572266, 5567.1704698, 6093.64182948]), + tolerance=5e-3, + ) + assert_near_equal( + p.get_val("fuel_flow", units="kg/s"), + np.array([0.12830921, 0.15107399, 0.24857052, 0.28013556, 0.24271405]), + tolerance=5e-3, + ) + assert_near_equal( + p.get_val("surge_margin"), + np.array([3.62385489, 5.13891739, 17.61488138, 29.65131358, 17.48630861]), + tolerance=5e-3, + ) + + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + @unittest.skipIf(skip_tests, "N+3 surrogate model has not been trained (cached data not found), so skipping N+3 tests") class N3TestCase(unittest.TestCase): def test_defaults(self): @@ -79,17 +93,17 @@ def test_defaults(self): p.setup(force_alloc_complex=True) - p.set_val('throttle', 0.5) - p.set_val('fltcond|h', 10e3, units='ft') - p.set_val('fltcond|M', 0.5) + p.set_val("throttle", 0.5) + p.set_val("fltcond|h", 10e3, units="ft") + p.set_val("fltcond|M", 0.5) p.run_model() - assert_near_equal(p.get_val('thrust', units='lbf'), 6902.32371562*np.ones(1), tolerance=1e-6) - assert_near_equal(p.get_val('fuel_flow', units='kg/s'), 0.35176628*np.ones(1), tolerance=1e-6) - assert_near_equal(p.get_val('surge_margin'), 18.42447377*np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("thrust", units="lbf"), 6902.32371562 * np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("fuel_flow", units="kg/s"), 0.35176628 * np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("surge_margin"), 18.42447377 * np.ones(1), tolerance=1e-6) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_vectorized(self): @@ -99,22 +113,31 @@ def test_vectorized(self): p.setup(force_alloc_complex=True) - p.set_val('throttle', np.linspace(0.0001, 1., nn)) - p.set_val('fltcond|h', np.linspace(0, 40e3, nn), units='ft') - p.set_val('fltcond|M', np.linspace(0.1, 0.9, nn)) + p.set_val("throttle", np.linspace(0.0001, 1.0, nn)) + p.set_val("fltcond|h", np.linspace(0, 40e3, nn), units="ft") + p.set_val("fltcond|M", np.linspace(0.1, 0.9, nn)) p.run_model() - assert_near_equal(p.get_val('thrust', units='lbf'), np.array([2116.01166807, 4298.21330183, - 5731.73026453, 5567.1916333, 6093.64182948]), tolerance=5e-3) - assert_near_equal(p.get_val('fuel_flow', units='kg/s'), np.array([0.15443523, 0.18527426, - 0.26886803, 0.29035632, 0.24271405]), tolerance=5e-3) - assert_near_equal(p.get_val('surge_margin'), np.array([9.63957356, 9.85223288, - 21.01375818, 31.54415929, 17.48630861]), tolerance=5e-3) - - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal( + p.get_val("thrust", units="lbf"), + np.array([2116.01166807, 4298.21330183, 5731.73026453, 5567.1916333, 6093.64182948]), + tolerance=5e-3, + ) + assert_near_equal( + p.get_val("fuel_flow", units="kg/s"), + np.array([0.15443523, 0.18527426, 0.26886803, 0.29035632, 0.24271405]), + tolerance=5e-3, + ) + assert_near_equal( + p.get_val("surge_margin"), + np.array([9.63957356, 9.85223288, 21.01375818, 31.54415929, 17.48630861]), + tolerance=5e-3, + ) + + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) -if __name__=="__main__": +if __name__ == "__main__": unittest.main() diff --git a/openconcept/propulsion/tests/test_cfm56.py b/openconcept/propulsion/tests/test_cfm56.py index ae00c6eb..6972502c 100644 --- a/openconcept/propulsion/tests/test_cfm56.py +++ b/openconcept/propulsion/tests/test_cfm56.py @@ -7,15 +7,18 @@ from openconcept.propulsion import CFM56 # Skip these test cases if the cached surrogate files don't exist -file_root = openconcept.__path__[0] + r'/propulsion/empirical_data/cfm56/' -cached_thrust = os.path.exists(file_root + 'cfm56thrust_trained.zip') -cached_fuelburn = os.path.exists(file_root + 'cfm56fuelburn_trained.zip') -cached_T4 = os.path.exists(file_root + 'cfm56T4_trained.zip') +file_root = openconcept.__path__[0] + r"/propulsion/empirical_data/cfm56/" +cached_thrust = os.path.exists(file_root + "cfm56thrust_trained.zip") +cached_fuelburn = os.path.exists(file_root + "cfm56fuelburn_trained.zip") +cached_T4 = os.path.exists(file_root + "cfm56T4_trained.zip") skip_tests = True if cached_thrust and cached_fuelburn and cached_T4: skip_tests = False -@unittest.skipIf(skip_tests, "CFM56 surrogate model has not been trained (cached data not found), so skipping CFM56 tests") + +@unittest.skipIf( + skip_tests, "CFM56 surrogate model has not been trained (cached data not found), so skipping CFM56 tests" +) class CFM56TestCase(unittest.TestCase): def test_defaults(self): p = Problem() @@ -23,17 +26,17 @@ def test_defaults(self): p.setup(force_alloc_complex=True) - p.set_val('throttle', 0.5) - p.set_val('fltcond|h', 10e3, units='ft') - p.set_val('fltcond|M', 0.5) + p.set_val("throttle", 0.5) + p.set_val("fltcond|h", 10e3, units="ft") + p.set_val("fltcond|M", 0.5) p.run_model() - assert_near_equal(p.get_val('thrust', units='lbf'), 7050.73840869*np.ones(1), tolerance=1e-6) - assert_near_equal(p.get_val('fuel_flow', units='kg/s'), 0.50273824*np.ones(1), tolerance=1e-6) - assert_near_equal(p.get_val('T4', units='degK'), 1432.06813946*np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("thrust", units="lbf"), 7050.73840869 * np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("fuel_flow", units="kg/s"), 0.50273824 * np.ones(1), tolerance=1e-6) + assert_near_equal(p.get_val("T4", units="degK"), 1432.06813946 * np.ones(1), tolerance=1e-6) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_vectorized(self): @@ -43,22 +46,31 @@ def test_vectorized(self): p.setup(force_alloc_complex=True) - p.set_val('throttle', np.linspace(0.0001, 1., nn)) - p.set_val('fltcond|h', np.linspace(0, 40e3, nn), units='ft') - p.set_val('fltcond|M', np.linspace(0.1, 0.9, nn)) + p.set_val("throttle", np.linspace(0.0001, 1.0, nn)) + p.set_val("fltcond|h", np.linspace(0, 40e3, nn), units="ft") + p.set_val("fltcond|M", np.linspace(0.1, 0.9, nn)) p.run_model() - assert_near_equal(p.get_val('thrust', units='lbf'), np.array([1445.41349482, 3961.46624224, - 5278.43191982, 5441.44404298, 6479.00525867]), tolerance=5e-3) - assert_near_equal(p.get_val('fuel_flow', units='kg/s'), np.array([0.17032429, 0.25496437, - 0.35745638, 0.40572545, 0.4924194]), tolerance=5e-3) - assert_near_equal(p.get_val('T4', units='degK'), np.array([1005.38911171, 1207.57548728, - 1381.94820904, 1508.07901676, 1665.37063872]), tolerance=5e-3) + assert_near_equal( + p.get_val("thrust", units="lbf"), + np.array([1445.41349482, 3961.46624224, 5278.43191982, 5441.44404298, 6479.00525867]), + tolerance=5e-3, + ) + assert_near_equal( + p.get_val("fuel_flow", units="kg/s"), + np.array([0.17032429, 0.25496437, 0.35745638, 0.40572545, 0.4924194]), + tolerance=5e-3, + ) + assert_near_equal( + p.get_val("T4", units="degK"), + np.array([1005.38911171, 1207.57548728, 1381.94820904, 1508.07901676, 1665.37063872]), + tolerance=5e-3, + ) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) -if __name__=="__main__": +if __name__ == "__main__": unittest.main() diff --git a/openconcept/propulsion/tests/test_simple_comps.py b/openconcept/propulsion/tests/test_simple_comps.py index fb2c2603..dba00572 100644 --- a/openconcept/propulsion/tests/test_simple_comps.py +++ b/openconcept/propulsion/tests/test_simple_comps.py @@ -4,216 +4,237 @@ from openmdao.api import IndepVarComp, Group, Problem from openconcept.propulsion import SimpleGenerator, SimpleMotor, SimplePropeller, SimpleTurboshaft, PowerSplit + class MotorTestGroup(Group): """ Test the motor component """ + def initialize(self): - self.options.declare('vec_size',default=1,desc="Number of mission analysis points to run") - self.options.declare('efficiency', default=1., desc='Efficiency (dimensionless)') - self.options.declare('weight_inc', default=1./5000, desc='kg/W') # 5kW/kg motors have been demoed - self.options.declare('weight_base', default=0., desc='kg base weight') - self.options.declare('cost_inc', default=100.0/745.0, desc='$ cost per watt') - self.options.declare('cost_base', default=1., desc= '$ cost base') - self.options.declare('use_defaults', default=True) + self.options.declare("vec_size", default=1, desc="Number of mission analysis points to run") + self.options.declare("efficiency", default=1.0, desc="Efficiency (dimensionless)") + self.options.declare("weight_inc", default=1.0 / 5000, desc="kg/W") # 5kW/kg motors have been demoed + self.options.declare("weight_base", default=0.0, desc="kg base weight") + self.options.declare("cost_inc", default=100.0 / 745.0, desc="$ cost per watt") + self.options.declare("cost_base", default=1.0, desc="$ cost base") + self.options.declare("use_defaults", default=True) def setup(self): - use_defaults = self.options['use_defaults'] - nn = self.options['vec_size'] + use_defaults = self.options["use_defaults"] + nn = self.options["vec_size"] if not use_defaults: - eta_b = self.options['efficiency'] - wi = self.options['weight_inc'] - wb = self.options['weight_base'] - ci = self.options['cost_inc'] - cb = self.options['cost_base'] - self.add_subsystem('motor', SimpleMotor(num_nodes=nn, - efficiency=eta_b, - weight_inc=wi, - weight_base=wb, - cost_inc=ci, - cost_base=cb)) + eta_b = self.options["efficiency"] + wi = self.options["weight_inc"] + wb = self.options["weight_base"] + ci = self.options["cost_inc"] + cb = self.options["cost_base"] + self.add_subsystem( + "motor", + SimpleMotor(num_nodes=nn, efficiency=eta_b, weight_inc=wi, weight_base=wb, cost_inc=ci, cost_base=cb), + ) else: - self.add_subsystem('motor', SimpleMotor(num_nodes=nn)) + self.add_subsystem("motor", SimpleMotor(num_nodes=nn)) + + iv = self.add_subsystem("iv", IndepVarComp()) + iv.add_output("elec_power_rating", val=100, units="kW") + iv.add_output("throttle", val=np.ones(nn) * 0.9) + self.connect("iv.elec_power_rating", "motor.elec_power_rating") + self.connect("iv.throttle", "motor.throttle") - iv = self.add_subsystem('iv', IndepVarComp()) - iv.add_output('elec_power_rating', val=100, units='kW') - iv.add_output('throttle', val=np.ones(nn)*0.9) - self.connect('iv.elec_power_rating','motor.elec_power_rating') - self.connect('iv.throttle','motor.throttle') class GeneratorTestGroup(Group): """ Test the generator component """ + def initialize(self): - self.options.declare('vec_size',default=1,desc="Number of mission analysis points to run") - self.options.declare('efficiency', default=1., desc='Efficiency (dimensionless)') - self.options.declare('weight_inc', default=1./5000, desc='kg/W') - self.options.declare('weight_base', default=0., desc='kg base weight') - self.options.declare('cost_inc', default=100.0/745.0, desc='$ cost per watt') - self.options.declare('cost_base', default=1., desc= '$ cost base') - self.options.declare('use_defaults', default=True) + self.options.declare("vec_size", default=1, desc="Number of mission analysis points to run") + self.options.declare("efficiency", default=1.0, desc="Efficiency (dimensionless)") + self.options.declare("weight_inc", default=1.0 / 5000, desc="kg/W") + self.options.declare("weight_base", default=0.0, desc="kg base weight") + self.options.declare("cost_inc", default=100.0 / 745.0, desc="$ cost per watt") + self.options.declare("cost_base", default=1.0, desc="$ cost base") + self.options.declare("use_defaults", default=True) def setup(self): - use_defaults = self.options['use_defaults'] - nn = self.options['vec_size'] + use_defaults = self.options["use_defaults"] + nn = self.options["vec_size"] if not use_defaults: - eta_b = self.options['efficiency'] - wi = self.options['weight_inc'] - wb = self.options['weight_base'] - ci = self.options['cost_inc'] - cb = self.options['cost_base'] - self.add_subsystem('generator', SimpleGenerator(num_nodes=nn, - efficiency=eta_b, - weight_inc=wi, - weight_base=wb, - cost_inc=ci, - cost_base=cb)) + eta_b = self.options["efficiency"] + wi = self.options["weight_inc"] + wb = self.options["weight_base"] + ci = self.options["cost_inc"] + cb = self.options["cost_base"] + self.add_subsystem( + "generator", + SimpleGenerator( + num_nodes=nn, efficiency=eta_b, weight_inc=wi, weight_base=wb, cost_inc=ci, cost_base=cb + ), + ) else: - self.add_subsystem('generator', SimpleGenerator(num_nodes=nn)) + self.add_subsystem("generator", SimpleGenerator(num_nodes=nn)) + + iv = self.add_subsystem("iv", IndepVarComp()) + iv.add_output("elec_power_rating", val=100, units="kW") + iv.add_output("shaft_power_in", val=np.ones(nn) * 90, units="kW") + self.connect("iv.elec_power_rating", "generator.elec_power_rating") + self.connect("iv.shaft_power_in", "generator.shaft_power_in") - iv = self.add_subsystem('iv', IndepVarComp()) - iv.add_output('elec_power_rating', val=100, units='kW') - iv.add_output('shaft_power_in', val=np.ones(nn)*90, units='kW') - self.connect('iv.elec_power_rating','generator.elec_power_rating') - self.connect('iv.shaft_power_in','generator.shaft_power_in') class TurboshaftTestGroup(Group): """ Test the turboshaft component """ + def initialize(self): - self.options.declare('vec_size',default=1,desc="Number of mission analysis points to run") - self.options.declare('psfc', default=0.6 * 1.68965774e-7, desc='power specific fuel consumption') - self.options.declare('weight_inc', default=0., desc='kg per watt') - self.options.declare('weight_base', default=0., desc='kg base weight') - self.options.declare('cost_inc', default=1.04, desc='$ cost per watt') - self.options.declare('cost_base', default=0., desc='$ cost base') - self.options.declare('use_defaults', default=True) + self.options.declare("vec_size", default=1, desc="Number of mission analysis points to run") + self.options.declare("psfc", default=0.6 * 1.68965774e-7, desc="power specific fuel consumption") + self.options.declare("weight_inc", default=0.0, desc="kg per watt") + self.options.declare("weight_base", default=0.0, desc="kg base weight") + self.options.declare("cost_inc", default=1.04, desc="$ cost per watt") + self.options.declare("cost_base", default=0.0, desc="$ cost base") + self.options.declare("use_defaults", default=True) def setup(self): - use_defaults = self.options['use_defaults'] - nn = self.options['vec_size'] + use_defaults = self.options["use_defaults"] + nn = self.options["vec_size"] if not use_defaults: - psfc = self.options['psfc'] - wi = self.options['weight_inc'] - wb = self.options['weight_base'] - ci = self.options['cost_inc'] - cb = self.options['cost_base'] - self.add_subsystem('turboshaft', SimpleTurboshaft(num_nodes=nn, - psfc=psfc, - weight_inc=wi, - weight_base=wb, - cost_inc=ci, - cost_base=cb)) + psfc = self.options["psfc"] + wi = self.options["weight_inc"] + wb = self.options["weight_base"] + ci = self.options["cost_inc"] + cb = self.options["cost_base"] + self.add_subsystem( + "turboshaft", + SimpleTurboshaft(num_nodes=nn, psfc=psfc, weight_inc=wi, weight_base=wb, cost_inc=ci, cost_base=cb), + ) else: - self.add_subsystem('turboshaft', SimpleTurboshaft(num_nodes=nn)) + self.add_subsystem("turboshaft", SimpleTurboshaft(num_nodes=nn)) - iv = self.add_subsystem('iv', IndepVarComp()) - iv.add_output('shaft_power_rating', val=1000, units='hp') - iv.add_output('throttle', val=np.ones(nn)*0.90) - self.connect('iv.shaft_power_rating','turboshaft.shaft_power_rating') - self.connect('iv.throttle','turboshaft.throttle') + iv = self.add_subsystem("iv", IndepVarComp()) + iv.add_output("shaft_power_rating", val=1000, units="hp") + iv.add_output("throttle", val=np.ones(nn) * 0.90) + self.connect("iv.shaft_power_rating", "turboshaft.shaft_power_rating") + self.connect("iv.throttle", "turboshaft.throttle") -class SimpleMotorTestCase(unittest.TestCase): +class SimpleMotorTestCase(unittest.TestCase): def test_default_settings(self): - prob = Problem(MotorTestGroup(vec_size=10, - use_defaults=True)) - prob.setup(check=True,force_alloc_complex=True) + prob = Problem(MotorTestGroup(vec_size=10, use_defaults=True)) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob.get_val('motor.shaft_power_out', units='kW'), np.ones(10)*90, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.elec_load', units='kW'), np.ones(10)*90, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.heat_out', units='kW'), np.ones(10)*0.0, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.component_sizing_margin'), np.ones(10)*0.90, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.component_cost', units='USD'), 13423.818791946309, tolerance=1e-10) - assert_near_equal(prob.get_val('motor.component_weight', units='kg'), 20, tolerance=1e-15) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("motor.shaft_power_out", units="kW"), np.ones(10) * 90, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.elec_load", units="kW"), np.ones(10) * 90, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.heat_out", units="kW"), np.ones(10) * 0.0, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.component_sizing_margin"), np.ones(10) * 0.90, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.component_cost", units="USD"), 13423.818791946309, tolerance=1e-10) + assert_near_equal(prob.get_val("motor.component_weight", units="kg"), 20, tolerance=1e-15) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_nondefault_settings(self): - prob = Problem(MotorTestGroup(vec_size=10, - use_defaults=False, - efficiency=0.95, - weight_inc=1/3000, - weight_base=2, - cost_inc=1/500, - cost_base=3, - )) - prob.setup(check=True,force_alloc_complex=True) + prob = Problem( + MotorTestGroup( + vec_size=10, + use_defaults=False, + efficiency=0.95, + weight_inc=1 / 3000, + weight_base=2, + cost_inc=1 / 500, + cost_base=3, + ) + ) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob.get_val('motor.shaft_power_out', units='kW'), np.ones(10)*90*0.95, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.elec_load', units='kW'), np.ones(10)*90, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.heat_out', units='kW'), np.ones(10)*90*0.05, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.component_sizing_margin'), np.ones(10)*0.90, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.component_cost', units='USD'), 203, tolerance=1e-15) - assert_near_equal(prob.get_val('motor.component_weight', units='kg'), 35.333333333333333333, tolerance=1e-10) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("motor.shaft_power_out", units="kW"), np.ones(10) * 90 * 0.95, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.elec_load", units="kW"), np.ones(10) * 90, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.heat_out", units="kW"), np.ones(10) * 90 * 0.05, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.component_sizing_margin"), np.ones(10) * 0.90, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.component_cost", units="USD"), 203, tolerance=1e-15) + assert_near_equal(prob.get_val("motor.component_weight", units="kg"), 35.333333333333333333, tolerance=1e-10) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) -class SimpleGeneratorTestCase(unittest.TestCase): +class SimpleGeneratorTestCase(unittest.TestCase): def test_default_settings(self): - prob = Problem(GeneratorTestGroup(vec_size=10, - use_defaults=True)) - prob.setup(check=True,force_alloc_complex=True) + prob = Problem(GeneratorTestGroup(vec_size=10, use_defaults=True)) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob.get_val('generator.elec_power_out', units='kW'), np.ones(10)*90, tolerance=1e-15) - assert_near_equal(prob.get_val('generator.heat_out', units='kW'), np.ones(10)*0.0, tolerance=1e-15) - assert_near_equal(prob.get_val('generator.component_sizing_margin'), np.ones(10)*0.90, tolerance=1e-15) - assert_near_equal(prob.get_val('generator.component_cost', units='USD'), 13423.818791946309, tolerance=1e-10) - assert_near_equal(prob.get_val('generator.component_weight', units='kg'), 20, tolerance=1e-15) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("generator.elec_power_out", units="kW"), np.ones(10) * 90, tolerance=1e-15) + assert_near_equal(prob.get_val("generator.heat_out", units="kW"), np.ones(10) * 0.0, tolerance=1e-15) + assert_near_equal(prob.get_val("generator.component_sizing_margin"), np.ones(10) * 0.90, tolerance=1e-15) + assert_near_equal(prob.get_val("generator.component_cost", units="USD"), 13423.818791946309, tolerance=1e-10) + assert_near_equal(prob.get_val("generator.component_weight", units="kg"), 20, tolerance=1e-15) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_nondefault_settings(self): - prob = Problem(GeneratorTestGroup(vec_size=10, - use_defaults=False, - efficiency=0.95, - weight_inc=1/3000, - weight_base=2, - cost_inc=1/500, - cost_base=3, - )) - prob.setup(check=True,force_alloc_complex=True) + prob = Problem( + GeneratorTestGroup( + vec_size=10, + use_defaults=False, + efficiency=0.95, + weight_inc=1 / 3000, + weight_base=2, + cost_inc=1 / 500, + cost_base=3, + ) + ) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob.get_val('generator.elec_power_out', units='kW'), np.ones(10)*90*0.95, tolerance=1e-15) - assert_near_equal(prob.get_val('generator.heat_out', units='kW'), np.ones(10)*90*0.05, tolerance=1e-15) - assert_near_equal(prob.get_val('generator.component_sizing_margin'), np.ones(10)*0.90*0.95, tolerance=1e-15) - assert_near_equal(prob.get_val('generator.component_cost', units='USD'), 203, tolerance=1e-15) - assert_near_equal(prob.get_val('generator.component_weight', units='kg'), 35.333333333333333333, tolerance=1e-10) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal( + prob.get_val("generator.elec_power_out", units="kW"), np.ones(10) * 90 * 0.95, tolerance=1e-15 + ) + assert_near_equal(prob.get_val("generator.heat_out", units="kW"), np.ones(10) * 90 * 0.05, tolerance=1e-15) + assert_near_equal(prob.get_val("generator.component_sizing_margin"), np.ones(10) * 0.90 * 0.95, tolerance=1e-15) + assert_near_equal(prob.get_val("generator.component_cost", units="USD"), 203, tolerance=1e-15) + assert_near_equal( + prob.get_val("generator.component_weight", units="kg"), 35.333333333333333333, tolerance=1e-10 + ) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) -class SimpleTurboshaftTestCase(unittest.TestCase): +class SimpleTurboshaftTestCase(unittest.TestCase): def test_default_settings(self): - prob = Problem(TurboshaftTestGroup(vec_size=10, - use_defaults=True)) - prob.setup(check=True,force_alloc_complex=True) + prob = Problem(TurboshaftTestGroup(vec_size=10, use_defaults=True)) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob.get_val('turboshaft.shaft_power_out', units='hp'), np.ones(10)*1000*0.9, tolerance=1e-6) - assert_near_equal(prob.get_val('turboshaft.fuel_flow', units='lbm/h'), np.ones(10)*0.6*1000*0.9, tolerance=1e-6) - assert_near_equal(prob.get_val('turboshaft.component_sizing_margin'), np.ones(10)*0.90, tolerance=1e-15) - assert_near_equal(prob.get_val('turboshaft.component_cost', units='USD'), 775528., tolerance=1e-7) - assert_near_equal(prob.get_val('turboshaft.component_weight', units='kg'), 0, tolerance=1e-15) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal( + prob.get_val("turboshaft.shaft_power_out", units="hp"), np.ones(10) * 1000 * 0.9, tolerance=1e-6 + ) + assert_near_equal( + prob.get_val("turboshaft.fuel_flow", units="lbm/h"), np.ones(10) * 0.6 * 1000 * 0.9, tolerance=1e-6 + ) + assert_near_equal(prob.get_val("turboshaft.component_sizing_margin"), np.ones(10) * 0.90, tolerance=1e-15) + assert_near_equal(prob.get_val("turboshaft.component_cost", units="USD"), 775528.0, tolerance=1e-7) + assert_near_equal(prob.get_val("turboshaft.component_weight", units="kg"), 0, tolerance=1e-15) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_nondefault_settings(self): - prob = Problem(TurboshaftTestGroup(vec_size=10, - use_defaults=False, - psfc=0.4*1.68965774e-7, - weight_inc=1/745.7, # 1 kg/hp - weight_base=50, - cost_inc=1000/745.7, # 1000 / hp - cost_base=50000)) - prob.setup(check=True,force_alloc_complex=True) + prob = Problem( + TurboshaftTestGroup( + vec_size=10, + use_defaults=False, + psfc=0.4 * 1.68965774e-7, + weight_inc=1 / 745.7, # 1 kg/hp + weight_base=50, + cost_inc=1000 / 745.7, # 1000 / hp + cost_base=50000, + ) + ) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob.get_val('turboshaft.shaft_power_out', units='hp'), np.ones(10)*1000*0.9, tolerance=1e-6) - assert_near_equal(prob.get_val('turboshaft.fuel_flow', units='lbm/h'), np.ones(10)*0.4*1000*0.9, tolerance=1e-6) - assert_near_equal(prob.get_val('turboshaft.component_sizing_margin'), np.ones(10)*0.90, tolerance=1e-15) - assert_near_equal(prob.get_val('turboshaft.component_cost', units='USD'), 1e6+50000., tolerance=1e-6) - assert_near_equal(prob.get_val('turboshaft.component_weight', units='kg'), 1050, tolerance=1e-10) - partials = prob.check_partials(method='cs',compact_print=True) - assert_check_partials(partials) \ No newline at end of file + assert_near_equal( + prob.get_val("turboshaft.shaft_power_out", units="hp"), np.ones(10) * 1000 * 0.9, tolerance=1e-6 + ) + assert_near_equal( + prob.get_val("turboshaft.fuel_flow", units="lbm/h"), np.ones(10) * 0.4 * 1000 * 0.9, tolerance=1e-6 + ) + assert_near_equal(prob.get_val("turboshaft.component_sizing_margin"), np.ones(10) * 0.90, tolerance=1e-15) + assert_near_equal(prob.get_val("turboshaft.component_cost", units="USD"), 1e6 + 50000.0, tolerance=1e-6) + assert_near_equal(prob.get_val("turboshaft.component_weight", units="kg"), 1050, tolerance=1e-10) + partials = prob.check_partials(method="cs", compact_print=True) + assert_check_partials(partials) diff --git a/openconcept/propulsion/tests/test_splitter_comps.py b/openconcept/propulsion/tests/test_splitter_comps.py index c7a3fefc..d3cfd980 100644 --- a/openconcept/propulsion/tests/test_splitter_comps.py +++ b/openconcept/propulsion/tests/test_splitter_comps.py @@ -12,57 +12,59 @@ def test_default_settings(self): p.setup(check=True, force_alloc_complex=True) p.run_model() - assert_near_equal(p['power_out_A'], np.array([0.5])) - assert_near_equal(p['power_out_B'], np.array([0.5])) - assert_near_equal(p['heat_out'], np.array([0.])) - assert_near_equal(p['component_cost'], np.array([0.])) - assert_near_equal(p['component_weight'], np.array([0.])) - assert_near_equal(p['component_sizing_margin'], np.array([1/99999999])) + assert_near_equal(p["power_out_A"], np.array([0.5])) + assert_near_equal(p["power_out_B"], np.array([0.5])) + assert_near_equal(p["heat_out"], np.array([0.0])) + assert_near_equal(p["component_cost"], np.array([0.0])) + assert_near_equal(p["component_weight"], np.array([0.0])) + assert_near_equal(p["component_sizing_margin"], np.array([1 / 99999999])) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_fraction(self): p = Problem() - p.model = PowerSplit(num_nodes=3, efficiency=0.95, weight_inc=0.01, - weight_base=1., cost_inc=0.02, cost_base=2.) + p.model = PowerSplit( + num_nodes=3, efficiency=0.95, weight_inc=0.01, weight_base=1.0, cost_inc=0.02, cost_base=2.0 + ) p.setup(check=True, force_alloc_complex=True) - p.set_val('power_in', np.array([1, 2, 3]), units='W') - p.set_val('power_rating', 10., units='W') - p.set_val('power_split_fraction', np.array([0.2, 0.4, 0.3])) + p.set_val("power_in", np.array([1, 2, 3]), units="W") + p.set_val("power_rating", 10.0, units="W") + p.set_val("power_split_fraction", np.array([0.2, 0.4, 0.3])) p.run_model() - assert_near_equal(p['power_out_A'], 0.95 * np.array([0.2, 0.8, 0.9])) - assert_near_equal(p['power_out_B'], 0.95 * np.array([0.8, 1.2, 2.1])) - assert_near_equal(p['heat_out'], 0.05 * np.array([1., 2., 3.])) - assert_near_equal(p['component_cost'], 2.2) - assert_near_equal(p['component_weight'], 1.1) - assert_near_equal(p['component_sizing_margin'], np.array([.1, .2, .3])) + assert_near_equal(p["power_out_A"], 0.95 * np.array([0.2, 0.8, 0.9])) + assert_near_equal(p["power_out_B"], 0.95 * np.array([0.8, 1.2, 2.1])) + assert_near_equal(p["heat_out"], 0.05 * np.array([1.0, 2.0, 3.0])) + assert_near_equal(p["component_cost"], 2.2) + assert_near_equal(p["component_weight"], 1.1) + assert_near_equal(p["component_sizing_margin"], np.array([0.1, 0.2, 0.3])) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_fixed(self): p = Problem() - p.model = PowerSplit(num_nodes=3, rule='fixed', efficiency=0.95, weight_inc=0.01, - weight_base=1., cost_inc=0.02, cost_base=2.) + p.model = PowerSplit( + num_nodes=3, rule="fixed", efficiency=0.95, weight_inc=0.01, weight_base=1.0, cost_inc=0.02, cost_base=2.0 + ) p.setup(check=True) - p.set_val('power_in', np.array([1, 2, 3]), units='W') - p.set_val('power_rating', 10., units='W') - p.set_val('power_split_amount', np.array([0.95, 1., 1.])) + p.set_val("power_in", np.array([1, 2, 3]), units="W") + p.set_val("power_rating", 10.0, units="W") + p.set_val("power_split_amount", np.array([0.95, 1.0, 1.0])) p.run_model() - assert_near_equal(p['power_out_A'], 0.95 * np.array([0.95, 1., 1.])) - assert_near_equal(p['power_out_B'], 0.95 * (np.array([1., 2., 3.]) - np.array([0.95, 1., 1.]))) - assert_near_equal(p['heat_out'], 0.05 * np.array([1., 2., 3.])) - assert_near_equal(p['component_cost'], 2.2) - assert_near_equal(p['component_weight'], 1.1) - assert_near_equal(p['component_sizing_margin'], np.array([.1, .2, .3])) + assert_near_equal(p["power_out_A"], 0.95 * np.array([0.95, 1.0, 1.0])) + assert_near_equal(p["power_out_B"], 0.95 * (np.array([1.0, 2.0, 3.0]) - np.array([0.95, 1.0, 1.0]))) + assert_near_equal(p["heat_out"], 0.05 * np.array([1.0, 2.0, 3.0])) + assert_near_equal(p["component_cost"], 2.2) + assert_near_equal(p["component_weight"], 1.1) + assert_near_equal(p["component_sizing_margin"], np.array([0.1, 0.2, 0.3])) - partials = p.check_partials(method='fd',compact_print=True) # for some reason this one - # doesn't work with complex step + partials = p.check_partials(method="fd", compact_print=True) # for some reason this one + # doesn't work with complex step assert_check_partials(partials) diff --git a/openconcept/propulsion/turboshaft.py b/openconcept/propulsion/turboshaft.py index 4bb3fff6..b0bd0f27 100644 --- a/openconcept/propulsion/turboshaft.py +++ b/openconcept/propulsion/turboshaft.py @@ -59,63 +59,63 @@ class SimpleTurboshaft(ExplicitComponent): def initialize(self): # psfc conversion from g/kW/hr to kg/W/s = 2.777e-10 # psfc conversion from lbfuel/hp/hr to kg/W/s = 1.690e-7 - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('psfc', default=0.6 * 1.68965774e-7, - desc='power specific fuel consumption') - self.options.declare('weight_inc', default=0., desc='kg per watt') - self.options.declare('weight_base', default=0., desc='kg base weight') - self.options.declare('cost_inc', default=1.04, desc='$ cost per watt') - self.options.declare('cost_base', default=0., desc='$ cost base') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("psfc", default=0.6 * 1.68965774e-7, desc="power specific fuel consumption") + self.options.declare("weight_inc", default=0.0, desc="kg per watt") + self.options.declare("weight_base", default=0.0, desc="kg base weight") + self.options.declare("cost_inc", default=1.04, desc="$ cost per watt") + self.options.declare("cost_base", default=0.0, desc="$ cost base") def setup(self): - nn = self.options['num_nodes'] - self.add_input('throttle', desc='Throttle input (Fractional)', shape=(nn,)) - self.add_input('shaft_power_rating', units='W', desc='Rated shaft power') - - psfc = self.options['psfc'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] - - self.add_output('shaft_power_out', units='W', desc='Output shaft power', shape=(nn,)) - self.add_output('fuel_flow', units='kg/s', desc='Fuel flow in (kg fuel / s)', shape=(nn,)) - self.add_output('component_cost', units='USD', desc='Motor component cost') - self.add_output('component_weight', units='kg', desc='Motor component weight') - self.add_output('component_sizing_margin', desc='Fraction of rated power', shape=(nn,)) - - self.declare_partials('shaft_power_out', 'shaft_power_rating') - self.declare_partials('shaft_power_out', 'throttle', rows=range(nn), cols=range(nn)) - - self.declare_partials('fuel_flow', 'shaft_power_rating') - self.declare_partials('fuel_flow', 'throttle', rows=range(nn), cols=range(nn)) - - self.declare_partials('component_cost', 'shaft_power_rating', val=cost_inc) - self.declare_partials('component_weight', 'shaft_power_rating', val=weight_inc) - self.declare_partials('component_sizing_margin', 'throttle', - val=1.0 * np.ones(nn), rows=range(nn), cols=range(nn)) + nn = self.options["num_nodes"] + self.add_input("throttle", desc="Throttle input (Fractional)", shape=(nn,)) + self.add_input("shaft_power_rating", units="W", desc="Rated shaft power") + + psfc = self.options["psfc"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] + + self.add_output("shaft_power_out", units="W", desc="Output shaft power", shape=(nn,)) + self.add_output("fuel_flow", units="kg/s", desc="Fuel flow in (kg fuel / s)", shape=(nn,)) + self.add_output("component_cost", units="USD", desc="Motor component cost") + self.add_output("component_weight", units="kg", desc="Motor component weight") + self.add_output("component_sizing_margin", desc="Fraction of rated power", shape=(nn,)) + + self.declare_partials("shaft_power_out", "shaft_power_rating") + self.declare_partials("shaft_power_out", "throttle", rows=range(nn), cols=range(nn)) + + self.declare_partials("fuel_flow", "shaft_power_rating") + self.declare_partials("fuel_flow", "throttle", rows=range(nn), cols=range(nn)) + + self.declare_partials("component_cost", "shaft_power_rating", val=cost_inc) + self.declare_partials("component_weight", "shaft_power_rating", val=weight_inc) + self.declare_partials( + "component_sizing_margin", "throttle", val=1.0 * np.ones(nn), rows=range(nn), cols=range(nn) + ) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - psfc = self.options['psfc'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] - cost_inc = self.options['cost_inc'] - cost_base = self.options['cost_base'] - - a = inputs['throttle'] - b = inputs['shaft_power_rating'] + nn = self.options["num_nodes"] + psfc = self.options["psfc"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + cost_inc = self.options["cost_inc"] + cost_base = self.options["cost_base"] + + a = inputs["throttle"] + b = inputs["shaft_power_rating"] c = a * b - outputs['shaft_power_out'] = inputs['throttle'] * inputs['shaft_power_rating'] - outputs['fuel_flow'] = inputs['throttle'] * inputs['shaft_power_rating'] * psfc - outputs['component_cost'] = inputs['shaft_power_rating'] * cost_inc + cost_base - outputs['component_weight'] = inputs['shaft_power_rating'] * weight_inc + weight_base - outputs['component_sizing_margin'] = inputs['throttle'] + outputs["shaft_power_out"] = inputs["throttle"] * inputs["shaft_power_rating"] + outputs["fuel_flow"] = inputs["throttle"] * inputs["shaft_power_rating"] * psfc + outputs["component_cost"] = inputs["shaft_power_rating"] * cost_inc + cost_base + outputs["component_weight"] = inputs["shaft_power_rating"] * weight_inc + weight_base + outputs["component_sizing_margin"] = inputs["throttle"] def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - psfc = self.options['psfc'] - J['shaft_power_out', 'throttle'] = inputs['shaft_power_rating'] * np.ones(nn) - J['shaft_power_out', 'shaft_power_rating'] = inputs['throttle'] - J['fuel_flow', 'throttle'] = inputs['shaft_power_rating'] * psfc * np.ones(nn) - J['fuel_flow', 'shaft_power_rating'] = inputs['throttle'] * psfc + nn = self.options["num_nodes"] + psfc = self.options["psfc"] + J["shaft_power_out", "throttle"] = inputs["shaft_power_rating"] * np.ones(nn) + J["shaft_power_out", "shaft_power_rating"] = inputs["throttle"] + J["fuel_flow", "throttle"] = inputs["shaft_power_rating"] * psfc * np.ones(nn) + J["fuel_flow", "shaft_power_rating"] = inputs["throttle"] * psfc diff --git a/openconcept/thermal/battery_cooling.py b/openconcept/thermal/battery_cooling.py index 21591a96..f7c37337 100644 --- a/openconcept/thermal/battery_cooling.py +++ b/openconcept/thermal/battery_cooling.py @@ -4,7 +4,7 @@ class LiquidCooledBattery(om.Group): - """A battery with liquid cooling + """A battery with liquid cooling Inputs ------ @@ -36,7 +36,7 @@ class LiquidCooledBattery(om.Group): Battery core temperature (vector, K) T_surface : float Battery surface temperature (vector, K) - + Options ------- num_nodes : int @@ -66,44 +66,68 @@ class LiquidCooledBattery(om.Group): """ def initialize(self): - self.options.declare('quasi_steady', default=False, desc='Treat the component as quasi-steady or with thermal mass') - self.options.declare('num_nodes', default=1, desc='Number of quasi-steady points to runs') - self.options.declare('coolant_specific_heat', default=3801, desc='Coolant specific heat in J/kg/K') - self.options.declare('fluid_k', default=0.405, desc='Thermal conductivity of the fluid in W / mK') - self.options.declare('nusselt', default=7.54, desc='Hydraulic diameter Nusselt number') - - self.options.declare('cell_kr', default=0.3) # 0.455 for an 18650 cell, knocked down a bit - self.options.declare('cell_diameter', default=0.021) - self.options.declare('cell_height', default=0.070) - self.options.declare('cell_mass', default=0.070) - self.options.declare('cell_specific_heat', default=875.) - self.options.declare('battery_weight_fraction', default=0.65) + self.options.declare( + "quasi_steady", default=False, desc="Treat the component as quasi-steady or with thermal mass" + ) + self.options.declare("num_nodes", default=1, desc="Number of quasi-steady points to runs") + self.options.declare("coolant_specific_heat", default=3801, desc="Coolant specific heat in J/kg/K") + self.options.declare("fluid_k", default=0.405, desc="Thermal conductivity of the fluid in W / mK") + self.options.declare("nusselt", default=7.54, desc="Hydraulic diameter Nusselt number") + + self.options.declare("cell_kr", default=0.3) # 0.455 for an 18650 cell, knocked down a bit + self.options.declare("cell_diameter", default=0.021) + self.options.declare("cell_height", default=0.070) + self.options.declare("cell_mass", default=0.070) + self.options.declare("cell_specific_heat", default=875.0) + self.options.declare("battery_weight_fraction", default=0.65) + def setup(self): - nn = self.options['num_nodes'] - quasi_steady = self.options['quasi_steady'] - - self.add_subsystem('hex', BandolierCoolingSystem(num_nodes=nn, - coolant_specific_heat=self.options['coolant_specific_heat'], - fluid_k=self.options['fluid_k'], - nusselt=self.options['nusselt'], - cell_kr=self.options['cell_kr'], - cell_diameter=self.options['cell_diameter'], - cell_height=self.options['cell_height'], - cell_mass=self.options['cell_mass'], - cell_specific_heat=self.options['cell_specific_heat'], - battery_weight_fraction=self.options['battery_weight_fraction']), - promotes_inputs=['q_in', 'mdot_coolant', 'T_in', ('T_battery', 'T'), 'battery_weight', 'n_cpb', 't_channel'], - promotes_outputs=['T_core', 'T_surface', 'T_out', 'dTdt']) - + nn = self.options["num_nodes"] + quasi_steady = self.options["quasi_steady"] + + self.add_subsystem( + "hex", + BandolierCoolingSystem( + num_nodes=nn, + coolant_specific_heat=self.options["coolant_specific_heat"], + fluid_k=self.options["fluid_k"], + nusselt=self.options["nusselt"], + cell_kr=self.options["cell_kr"], + cell_diameter=self.options["cell_diameter"], + cell_height=self.options["cell_height"], + cell_mass=self.options["cell_mass"], + cell_specific_heat=self.options["cell_specific_heat"], + battery_weight_fraction=self.options["battery_weight_fraction"], + ), + promotes_inputs=[ + "q_in", + "mdot_coolant", + "T_in", + ("T_battery", "T"), + "battery_weight", + "n_cpb", + "t_channel", + ], + promotes_outputs=["T_core", "T_surface", "T_out", "dTdt"], + ) + if not quasi_steady: - ode_integ = self.add_subsystem('ode_integ', Integrator(num_nodes=nn, diff_units='s', method='simpson', time_setup='duration'), - promotes_outputs=['*'], promotes_inputs=['*']) - ode_integ.add_integrand('T', rate_name='dTdt', units='K', lower=1e-10) + ode_integ = self.add_subsystem( + "ode_integ", + Integrator(num_nodes=nn, diff_units="s", method="simpson", time_setup="duration"), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + ode_integ.add_integrand("T", rate_name="dTdt", units="K", lower=1e-10) else: - self.add_subsystem('thermal_bal', - om.BalanceComp('T', eq_units='K/s', lhs_name='dTdt', rhs_val=0.0, units='K', lower=1.0, val=299.*np.ones((nn,))), - promotes_inputs=['dTdt'], - promotes_outputs=['T']) + self.add_subsystem( + "thermal_bal", + om.BalanceComp( + "T", eq_units="K/s", lhs_name="dTdt", rhs_val=0.0, units="K", lower=1.0, val=299.0 * np.ones((nn,)) + ), + promotes_inputs=["dTdt"], + promotes_outputs=["T"], + ) class BandolierCoolingSystem(om.ExplicitComponent): @@ -181,75 +205,84 @@ class BandolierCoolingSystem(om.ExplicitComponent): battery_weight_fraction : float Fraction of battery by weight that is cells (default 0.72 knocks down Tesla by a bit) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - self.options.declare('coolant_specific_heat', default=3801, desc='Coolant specific heat in J/kg/K') - self.options.declare('fluid_k', default=0.405, desc='Thermal conductivity of the fluid in W / mK') - self.options.declare('nusselt', default=7.54, desc='Hydraulic diameter Nusselt number') - - self.options.declare('cell_kr', default=0.3) # 0.455 for an 18650 cell, knocked down a bit - self.options.declare('cell_diameter', default=0.021) - self.options.declare('cell_height', default=0.070) - self.options.declare('cell_mass', default=0.070) - self.options.declare('cell_specific_heat', default=875.) - self.options.declare('battery_weight_fraction', default=0.65) + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + self.options.declare("coolant_specific_heat", default=3801, desc="Coolant specific heat in J/kg/K") + self.options.declare("fluid_k", default=0.405, desc="Thermal conductivity of the fluid in W / mK") + self.options.declare("nusselt", default=7.54, desc="Hydraulic diameter Nusselt number") + + self.options.declare("cell_kr", default=0.3) # 0.455 for an 18650 cell, knocked down a bit + self.options.declare("cell_diameter", default=0.021) + self.options.declare("cell_height", default=0.070) + self.options.declare("cell_mass", default=0.070) + self.options.declare("cell_specific_heat", default=875.0) + self.options.declare("battery_weight_fraction", default=0.65) def setup(self): - nn = self.options['num_nodes'] - self.add_input('q_in', shape=(nn,), units='W', val=0.0) - self.add_input('T_in', shape=(nn,), units='K', val=300.) - self.add_input('T_battery', shape=(nn,), units='K', val=300.) - self.add_input('mdot_coolant', shape=(nn,), units='kg/s', val=0.20) - self.add_input('battery_weight', units='kg', val=478.) - self.add_input('n_cpb', units=None, val=82.) - self.add_input('t_channel', units='m', val=0.0005) - - self.add_output('dTdt', shape=(nn,), units='K/s', tags=['integrate', 'state_name:T_battery', 'state_units:K', 'state_val:300.0', 'state_promotes:True']) - self.add_output('T_surface', shape=(nn,), units='K', lower=1e-10) - self.add_output('T_core', shape=(nn,), units='K', lower=1e-10) - self.add_output('q', shape=(nn,), units='W') - self.add_output('T_out', shape=(nn,), units='K', val=300, lower=1e-10) - - self.declare_partials(['*'], ['*'], method='cs') + nn = self.options["num_nodes"] + self.add_input("q_in", shape=(nn,), units="W", val=0.0) + self.add_input("T_in", shape=(nn,), units="K", val=300.0) + self.add_input("T_battery", shape=(nn,), units="K", val=300.0) + self.add_input("mdot_coolant", shape=(nn,), units="kg/s", val=0.20) + self.add_input("battery_weight", units="kg", val=478.0) + self.add_input("n_cpb", units=None, val=82.0) + self.add_input("t_channel", units="m", val=0.0005) + + self.add_output( + "dTdt", + shape=(nn,), + units="K/s", + tags=["integrate", "state_name:T_battery", "state_units:K", "state_val:300.0", "state_promotes:True"], + ) + self.add_output("T_surface", shape=(nn,), units="K", lower=1e-10) + self.add_output("T_core", shape=(nn,), units="K", lower=1e-10) + self.add_output("q", shape=(nn,), units="W") + self.add_output("T_out", shape=(nn,), units="K", val=300, lower=1e-10) + + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - n_cells = inputs['battery_weight'] * self.options['battery_weight_fraction'] / self.options['cell_mass'] - n_bandoliers = n_cells / inputs['n_cpb'] / 2 + nn = self.options["num_nodes"] + n_cells = inputs["battery_weight"] * self.options["battery_weight_fraction"] / self.options["cell_mass"] + n_bandoliers = n_cells / inputs["n_cpb"] / 2 - mdot_b = inputs['mdot_coolant'] / n_bandoliers - q_cell = inputs['q_in'] / n_cells - hconv = self.options['nusselt'] * self.options['fluid_k'] / 2 / inputs['t_channel'] + mdot_b = inputs["mdot_coolant"] / n_bandoliers + q_cell = inputs["q_in"] / n_cells + hconv = self.options["nusselt"] * self.options["fluid_k"] / 2 / inputs["t_channel"] - Hc = self.options['cell_height'] - Dc = self.options['cell_diameter'] - mc = self.options['cell_mass'] - krc = self.options['cell_kr'] - cpc = self.options['cell_specific_heat'] - L_bandolier = inputs['n_cpb'] * Dc + Hc = self.options["cell_height"] + Dc = self.options["cell_diameter"] + mc = self.options["cell_mass"] + krc = self.options["cell_kr"] + cpc = self.options["cell_specific_heat"] + L_bandolier = inputs["n_cpb"] * Dc - cpf = self.options['coolant_specific_heat'] # of the coolant + cpf = self.options["coolant_specific_heat"] # of the coolant - A_heat_trans = Hc * L_bandolier * 2 # two sides of the tape + A_heat_trans = Hc * L_bandolier * 2 # two sides of the tape NTU = hconv * A_heat_trans / mdot_b / cpf - Kcell = mdot_b * cpf * (1 - np.exp(-NTU)) / 2 / inputs['n_cpb'] # divide out the total bandolier convection by 2 * n_cpb cells + Kcell = ( + mdot_b * cpf * (1 - np.exp(-NTU)) / 2 / inputs["n_cpb"] + ) # divide out the total bandolier convection by 2 * n_cpb cells # the convective heat transfer is (Ts - Tin) * Kcell PI = np.pi - - Tbar = inputs['T_battery'] + + Tbar = inputs["T_battery"] Rc = Dc / 2 - K_cyl = 8*np.pi*Hc*krc + K_cyl = 8 * np.pi * Hc * krc - Ts = (K_cyl * Tbar + Kcell * inputs['T_in']) / (K_cyl + Kcell) - - outputs['T_surface'] = Ts + Ts = (K_cyl * Tbar + Kcell * inputs["T_in"]) / (K_cyl + Kcell) - q_conv = (Ts - inputs['T_in']) * Kcell * n_cells - outputs['dTdt'] = (q_cell - (Ts - inputs['T_in']) * Kcell) / mc / cpc # todo check that this quantity matches convection + outputs["T_surface"] = Ts + q_conv = (Ts - inputs["T_in"]) * Kcell * n_cells + outputs["dTdt"] = ( + (q_cell - (Ts - inputs["T_in"]) * Kcell) / mc / cpc + ) # todo check that this quantity matches convection - outputs['q'] = q_conv + outputs["q"] = q_conv qcheck = (Tbar - Ts) * K_cyl # UAcomb = 1/(1/hconv/A_heat_trans+1/K_cyl/2/inputs['n_cpb']) @@ -259,5 +292,5 @@ def compute(self, inputs, outputs): # # the heat flux across the cell is not equal to the heat flux due to convection # raise ValueError('The surface temperature solution appears to be wrong') - outputs['T_out'] = inputs['T_in'] + outputs['q'] / inputs['mdot_coolant'] / cpf - outputs['T_core'] = (Tbar - Ts) + Tbar + outputs["T_out"] = inputs["T_in"] + outputs["q"] / inputs["mdot_coolant"] / cpf + outputs["T_core"] = (Tbar - Ts) + Tbar diff --git a/openconcept/thermal/chiller.py b/openconcept/thermal/chiller.py index 3e6d01ee..ce2fc8d6 100644 --- a/openconcept/thermal/chiller.py +++ b/openconcept/thermal/chiller.py @@ -4,6 +4,7 @@ from openconcept.utilities import LinearInterpolator from .thermal import PerfectHeatTransferComp + class LinearSelector(om.ExplicitComponent): """ Averages thermal parameters given bypass @@ -23,7 +24,7 @@ class LinearSelector(om.ExplicitComponent): bypass : float Bypass parameter in range 0 - 1 (inclusive); 0 represents full refrig and no bypass, 1 all bypass no refrig (vector, None) - + Outputs ------- T_out_cold : float @@ -32,59 +33,62 @@ class LinearSelector(om.ExplicitComponent): Outgoing coolant temperature on the hot side (vector, K) elec_load : float Electrical load (vector, W) - + Options ------- num_nodes : int The number of analysis points to run """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') + self.options.declare("num_nodes", default=1, desc="Number of analysis points") def setup(self): - nn = self.options['num_nodes'] - self.add_input('T_in_cold', val=300*np.ones((nn,)), units='K') - self.add_input('T_in_hot', val=305*np.ones((nn,)), units='K') - self.add_input('T_out_refrig_cold', val=290*np.ones((nn,)), units='K') - self.add_input('T_out_refrig_hot', val=310*np.ones((nn,)), units='K') - self.add_input('power_rating', units='W') - self.add_input('bypass', val=np.ones((nn,)), units=None) - - self.add_output('elec_load', val=np.ones((nn,))*1, units='W') - self.add_output('T_out_cold', val=290*np.ones((nn,)), units='K') - self.add_output('T_out_hot', val=310*np.ones((nn,)), units='K') - - self.declare_partials('T_out_cold', ['bypass', 'T_in_hot', 'T_out_refrig_cold'], - rows=np.arange(nn), cols=np.arange(nn)) - self.declare_partials('T_out_hot', ['bypass', 'T_in_cold', 'T_out_refrig_hot'], - rows=np.arange(nn), cols=np.arange(nn)) - self.declare_partials('elec_load', 'bypass', rows=np.arange(nn), cols=np.arange(nn)) - self.declare_partials('elec_load', 'power_rating', rows=np.arange(nn), cols=np.zeros(nn)) + nn = self.options["num_nodes"] + self.add_input("T_in_cold", val=300 * np.ones((nn,)), units="K") + self.add_input("T_in_hot", val=305 * np.ones((nn,)), units="K") + self.add_input("T_out_refrig_cold", val=290 * np.ones((nn,)), units="K") + self.add_input("T_out_refrig_hot", val=310 * np.ones((nn,)), units="K") + self.add_input("power_rating", units="W") + self.add_input("bypass", val=np.ones((nn,)), units=None) + + self.add_output("elec_load", val=np.ones((nn,)) * 1, units="W") + self.add_output("T_out_cold", val=290 * np.ones((nn,)), units="K") + self.add_output("T_out_hot", val=310 * np.ones((nn,)), units="K") + + self.declare_partials( + "T_out_cold", ["bypass", "T_in_hot", "T_out_refrig_cold"], rows=np.arange(nn), cols=np.arange(nn) + ) + self.declare_partials( + "T_out_hot", ["bypass", "T_in_cold", "T_out_refrig_hot"], rows=np.arange(nn), cols=np.arange(nn) + ) + self.declare_partials("elec_load", "bypass", rows=np.arange(nn), cols=np.arange(nn)) + self.declare_partials("elec_load", "power_rating", rows=np.arange(nn), cols=np.zeros(nn)) def compute(self, inputs, outputs): - bypass_side = inputs['bypass'] - refrig_side = 1 - inputs['bypass'] - outputs['T_out_cold'] = bypass_side * inputs['T_in_hot'] + refrig_side * inputs['T_out_refrig_cold'] - outputs['T_out_hot'] = bypass_side * inputs['T_in_cold'] + refrig_side * inputs['T_out_refrig_hot'] - outputs['elec_load'] = refrig_side * inputs['power_rating'] / 0.95 - + bypass_side = inputs["bypass"] + refrig_side = 1 - inputs["bypass"] + outputs["T_out_cold"] = bypass_side * inputs["T_in_hot"] + refrig_side * inputs["T_out_refrig_cold"] + outputs["T_out_hot"] = bypass_side * inputs["T_in_cold"] + refrig_side * inputs["T_out_refrig_hot"] + outputs["elec_load"] = refrig_side * inputs["power_rating"] / 0.95 + def compute_partials(self, inputs, J): - T_in_hot = inputs['T_in_hot'] - T_out_hot = inputs['T_out_refrig_hot'] - T_in_cold = inputs['T_in_cold'] - T_out_cold = inputs['T_out_refrig_cold'] - power_rating = inputs['power_rating'] - bypass = inputs['bypass'] - - J['T_out_cold', 'bypass'] = T_in_hot - T_out_cold - J['T_out_cold', 'T_in_hot'] = bypass - J['T_out_cold', 'T_out_refrig_cold'] = 1 - bypass - J['T_out_hot', 'bypass'] = T_in_cold - T_out_hot - J['T_out_hot', 'T_in_cold'] = bypass - J['T_out_hot', 'T_out_refrig_hot'] = 1 - bypass - J['elec_load', 'bypass'] = -power_rating / 0.95 - J['elec_load', 'power_rating'] = (1 - bypass) / 0.95 - + T_in_hot = inputs["T_in_hot"] + T_out_hot = inputs["T_out_refrig_hot"] + T_in_cold = inputs["T_in_cold"] + T_out_cold = inputs["T_out_refrig_cold"] + power_rating = inputs["power_rating"] + bypass = inputs["bypass"] + + J["T_out_cold", "bypass"] = T_in_hot - T_out_cold + J["T_out_cold", "T_in_hot"] = bypass + J["T_out_cold", "T_out_refrig_cold"] = 1 - bypass + J["T_out_hot", "bypass"] = T_in_cold - T_out_hot + J["T_out_hot", "T_in_cold"] = bypass + J["T_out_hot", "T_out_refrig_hot"] = 1 - bypass + J["elec_load", "bypass"] = -power_rating / 0.95 + J["elec_load", "power_rating"] = (1 - bypass) / 0.95 + class COPHeatPump(om.ExplicitComponent): """ @@ -109,37 +113,39 @@ class COPHeatPump(om.ExplicitComponent): num_nodes : int Number of analysis points to run (scalar, default 1) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') + self.options.declare("num_nodes", default=1, desc="Number of analysis points") def setup(self): - nn = self.options['num_nodes'] - self.add_input('COP', val=np.ones((nn,)), units=None) - self.add_input('power_rating', units='W', val=1000) + nn = self.options["num_nodes"] + self.add_input("COP", val=np.ones((nn,)), units=None) + self.add_input("power_rating", units="W", val=1000) - self.add_output('q_in_1', val=np.zeros((nn,)), units='W', shape=(nn,)) - self.add_output('q_in_2', val=np.zeros((nn,)), units='W', shape=(nn,)) + self.add_output("q_in_1", val=np.zeros((nn,)), units="W", shape=(nn,)) + self.add_output("q_in_2", val=np.zeros((nn,)), units="W", shape=(nn,)) - self.declare_partials(['q_in_1', 'q_in_2'], 'COP', rows=np.arange(nn), cols=np.arange(nn)) - self.declare_partials(['q_in_1', 'q_in_2'], 'power_rating', rows=np.arange(nn), cols=np.zeros(nn)) + self.declare_partials(["q_in_1", "q_in_2"], "COP", rows=np.arange(nn), cols=np.arange(nn)) + self.declare_partials(["q_in_1", "q_in_2"], "power_rating", rows=np.arange(nn), cols=np.zeros(nn)) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - outputs['q_in_1'] = - inputs['COP']*inputs['power_rating'] - outputs['q_in_2'] = (inputs['COP'] + 1)*inputs['power_rating'] - + nn = self.options["num_nodes"] + outputs["q_in_1"] = -inputs["COP"] * inputs["power_rating"] + outputs["q_in_2"] = (inputs["COP"] + 1) * inputs["power_rating"] + def compute_partials(self, inputs, J): - COP = inputs['COP'] - power_rating = inputs['power_rating'] + COP = inputs["COP"] + power_rating = inputs["power_rating"] + + J["q_in_1", "COP"] = -power_rating + J["q_in_1", "power_rating"] = -COP + J["q_in_2", "COP"] = power_rating + J["q_in_2", "power_rating"] = COP + 1 - J['q_in_1', 'COP'] = -power_rating - J['q_in_1', 'power_rating'] = -COP - J['q_in_2', 'COP'] = power_rating - J['q_in_2', 'power_rating'] = COP + 1 class HeatPumpWeight(om.ExplicitComponent): """ - Computes weight and power metrics for the vapor cycle machine. + Computes weight and power metrics for the vapor cycle machine. Defaults based on limited published data and guesswork. Inputs @@ -148,29 +154,29 @@ class HeatPumpWeight(om.ExplicitComponent): Rated electric power (scalar, W) specific_power : float Power per weight (scalar, W/kg) - + Outputs ------- component_weight : float Component weight (including coolants + motor) (scalar, kg) """ + def setup(self): - self.add_input('power_rating', val=1000.0, units='W') - self.add_input('specific_power', val=200., units='W/kg') - self.add_output('component_weight', val=0.0, units='kg') - self.declare_partials('component_weight',['power_rating','specific_power']) + self.add_input("power_rating", val=1000.0, units="W") + self.add_input("specific_power", val=200.0, units="W/kg") + self.add_output("component_weight", val=0.0, units="kg") + self.declare_partials("component_weight", ["power_rating", "specific_power"]) def compute(self, inputs, outputs): - outputs['component_weight'] = inputs['power_rating'] / inputs['specific_power'] - + outputs["component_weight"] = inputs["power_rating"] / inputs["specific_power"] + def compute_partials(self, inputs, J): - J['component_weight', 'power_rating'] = 1 / inputs['specific_power'] - J['component_weight', 'specific_power'] = -inputs['power_rating'] / inputs['specific_power'] ** 2 - - + J["component_weight", "power_rating"] = 1 / inputs["specific_power"] + J["component_weight", "specific_power"] = -inputs["power_rating"] / inputs["specific_power"] ** 2 + class HeatPumpWithIntegratedCoolantLoop(om.Group): - """ + """ Models chiller with integrated coolant inputs and outputs on the hot and cold sides. Can bypass the chiller using linearly interpolated control points control.bypass_start @@ -218,46 +224,64 @@ class HeatPumpWithIntegratedCoolantLoop(om.Group): specific_heat : float Specific heat of the coolant (scalar, J/kg/K, default 3801 glycol/water) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - self.options.declare('specific_heat', default=3801., desc='Specific heat in J/kg/K') + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + self.options.declare("specific_heat", default=3801.0, desc="Specific heat in J/kg/K") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] nn_ones = np.ones((nn,)) - spec_heat = self.options['specific_heat'] - - - iv = self.add_subsystem('control', om.IndepVarComp()) - iv.add_output('bypass_start',val=1.0) - iv.add_output('bypass_end',val=1.0) - - li = self.add_subsystem('li',LinearInterpolator(num_nodes=nn, units=None), promotes_outputs=[('vec', 'bypass')]) - self.connect('control.bypass_start','li.start_val') - self.connect('control.bypass_end','li.end_val') - self.add_subsystem('weightpower',HeatPumpWeight(), - promotes_inputs=['power_rating','specific_power'], promotes_outputs=['component_weight']) - - self.add_subsystem('hot_side', PerfectHeatTransferComp(num_nodes=nn, specific_heat=spec_heat), - promotes_inputs=[('T_in', 'T_in_hot'), 'mdot_coolant']) - self.add_subsystem('cold_side', PerfectHeatTransferComp(num_nodes=nn, specific_heat=spec_heat), - promotes_inputs=[('T_in', 'T_in_cold'), 'mdot_coolant']) - self.add_subsystem('copmatch', COPExplicit(num_nodes=nn), promotes_inputs=['eff_factor'], promotes_outputs=['COP']) - self.connect('hot_side.T_out','copmatch.T_h') - self.connect('cold_side.T_out','copmatch.T_c') - self.add_subsystem('heat_pump', COPHeatPump(num_nodes=nn), promotes_inputs=['power_rating','COP']) - self.connect('heat_pump.q_in_1','cold_side.q') - self.connect('heat_pump.q_in_2','hot_side.q') + spec_heat = self.options["specific_heat"] + + iv = self.add_subsystem("control", om.IndepVarComp()) + iv.add_output("bypass_start", val=1.0) + iv.add_output("bypass_end", val=1.0) + + li = self.add_subsystem( + "li", LinearInterpolator(num_nodes=nn, units=None), promotes_outputs=[("vec", "bypass")] + ) + self.connect("control.bypass_start", "li.start_val") + self.connect("control.bypass_end", "li.end_val") + self.add_subsystem( + "weightpower", + HeatPumpWeight(), + promotes_inputs=["power_rating", "specific_power"], + promotes_outputs=["component_weight"], + ) + + self.add_subsystem( + "hot_side", + PerfectHeatTransferComp(num_nodes=nn, specific_heat=spec_heat), + promotes_inputs=[("T_in", "T_in_hot"), "mdot_coolant"], + ) + self.add_subsystem( + "cold_side", + PerfectHeatTransferComp(num_nodes=nn, specific_heat=spec_heat), + promotes_inputs=[("T_in", "T_in_cold"), "mdot_coolant"], + ) + self.add_subsystem( + "copmatch", COPExplicit(num_nodes=nn), promotes_inputs=["eff_factor"], promotes_outputs=["COP"] + ) + self.connect("hot_side.T_out", "copmatch.T_h") + self.connect("cold_side.T_out", "copmatch.T_c") + self.add_subsystem("heat_pump", COPHeatPump(num_nodes=nn), promotes_inputs=["power_rating", "COP"]) + self.connect("heat_pump.q_in_1", "cold_side.q") + self.connect("heat_pump.q_in_2", "hot_side.q") # Set the default set points and T_in defaults for continuity - self.set_input_defaults('T_in_hot', val=400.*nn_ones, units='K') - self.set_input_defaults('T_in_cold', val=400.*nn_ones, units='K') + self.set_input_defaults("T_in_hot", val=400.0 * nn_ones, units="K") + self.set_input_defaults("T_in_cold", val=400.0 * nn_ones, units="K") + + self.add_subsystem( + "bypass_comp", + LinearSelector(num_nodes=nn), + promotes_inputs=["T_in_cold", "T_in_hot", "power_rating"], + promotes_outputs=["T_out_cold", "T_out_hot", "elec_load"], + ) + self.connect("hot_side.T_out", "bypass_comp.T_out_refrig_hot") + self.connect("cold_side.T_out", "bypass_comp.T_out_refrig_cold") + self.connect("bypass", "bypass_comp.bypass") - self.add_subsystem('bypass_comp',LinearSelector(num_nodes=nn), - promotes_inputs=['T_in_cold', 'T_in_hot','power_rating'], - promotes_outputs=['T_out_cold', 'T_out_hot','elec_load']) - self.connect('hot_side.T_out', 'bypass_comp.T_out_refrig_hot') - self.connect('cold_side.T_out', 'bypass_comp.T_out_refrig_cold') - self.connect('bypass', 'bypass_comp.bypass') class COPExplicit(om.ExplicitComponent): """ @@ -272,37 +296,38 @@ class COPExplicit(om.ExplicitComponent): Hot side temperature (vector, K) eff_factor : float Efficiency factor (scalar, None) - + Outputs ------- COP : float Coefficient of performance (vector, None) - + Options ------- num_nodes : int The number of analysis points to run """ + def initialize(self): - self.options.declare('num_nodes',default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] - self.add_input('T_c', val=300., units='K', shape=(nn,)) - self.add_input('T_h', val=400., units='K', shape=(nn,)) - self.add_input('eff_factor', units=None, val=0.4) + self.add_input("T_c", val=300.0, units="K", shape=(nn,)) + self.add_input("T_h", val=400.0, units="K", shape=(nn,)) + self.add_input("eff_factor", units=None, val=0.4) - self.add_output('COP', units=None, shape=(nn,), val=0.0) + self.add_output("COP", units=None, shape=(nn,), val=0.0) - self.declare_partials(['COP'], ['T_c','T_h','eff_factor'], method='cs') + self.declare_partials(["COP"], ["T_c", "T_h", "eff_factor"], method="cs") def compute(self, inputs, outputs): epsilon = 0.05 - delta_T = inputs['T_h'] - inputs['T_c'] - COP_raw = inputs['T_c'] / (delta_T) + delta_T = inputs["T_h"] - inputs["T_c"] + COP_raw = inputs["T_c"] / (delta_T) alpha = -1.5 - a = COP_raw*np.tanh(delta_T)*inputs['eff_factor'] + (1+np.tanh(-(delta_T+3)))/2*10 - b = 10. - COP_soft = (a*np.exp(alpha*a)+b*np.exp(alpha*b))/(np.exp(alpha*a)+np.exp(alpha*b)) - outputs['COP'] = COP_soft \ No newline at end of file + a = COP_raw * np.tanh(delta_T) * inputs["eff_factor"] + (1 + np.tanh(-(delta_T + 3))) / 2 * 10 + b = 10.0 + COP_soft = (a * np.exp(alpha * a) + b * np.exp(alpha * b)) / (np.exp(alpha * a) + np.exp(alpha * b)) + outputs["COP"] = COP_soft diff --git a/openconcept/thermal/ducts.py b/openconcept/thermal/ducts.py index 09443dfb..f96fa9ae 100644 --- a/openconcept/thermal/ducts.py +++ b/openconcept/thermal/ducts.py @@ -4,6 +4,7 @@ from .heat_exchanger import HXGroup from openconcept.utilities import AddSubtractComp, DVLabel + class ExplicitIncompressibleDuct(ExplicitComponent): """ This is a very approximate model of a duct at incompressible speeds. @@ -44,41 +45,44 @@ class ExplicitIncompressibleDuct(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes',default=1) - self.options.declare('static_pressure_loss_factor',default=0.15) - self.options.declare('gross_thrust_factor',default=0.98) + self.options.declare("num_nodes", default=1) + self.options.declare("static_pressure_loss_factor", default=0.15) + self.options.declare("gross_thrust_factor", default=0.98) def setup(self): - nn = self.options['num_nodes'] - self.add_input('fltcond|Utrue', shape=(nn,), units='m/s') - self.add_input('fltcond|rho', shape=(nn,), units='kg/m**3') - self.add_input('area_nozzle', shape=(nn,), units='m**2') - self.add_input('delta_p_hex', shape=(nn,), units='Pa') + nn = self.options["num_nodes"] + self.add_input("fltcond|Utrue", shape=(nn,), units="m/s") + self.add_input("fltcond|rho", shape=(nn,), units="kg/m**3") + self.add_input("area_nozzle", shape=(nn,), units="m**2") + self.add_input("delta_p_hex", shape=(nn,), units="Pa") - self.add_output('mdot', shape=(nn,), lower=0.01, units='kg/s') - self.add_output('drag', shape=(nn,), units='N') + self.add_output("mdot", shape=(nn,), lower=0.01, units="kg/s") + self.add_output("drag", shape=(nn,), units="N") # self.declare_partials(['drag','mdot'],['fltcond|Utrue','fltcond|rho','delta_p_hex'],rows=np.arange(nn),cols=np.arange(nn)) # self.declare_partials(['drag','mdot'],['area_nozzle'],rows=np.arange(nn),cols=np.zeros((nn,))) - self.declare_partials(['drag','mdot'],['fltcond|Utrue','fltcond|rho','delta_p_hex'],method='cs') - self.declare_partials(['drag','mdot'],['area_nozzle'],method='cs') + self.declare_partials(["drag", "mdot"], ["fltcond|Utrue", "fltcond|rho", "delta_p_hex"], method="cs") + self.declare_partials(["drag", "mdot"], ["area_nozzle"], method="cs") def compute(self, inputs, outputs): - static_pressure_loss_factor = self.options['static_pressure_loss_factor'] - cfg = self.options['gross_thrust_factor'] + static_pressure_loss_factor = self.options["static_pressure_loss_factor"] + cfg = self.options["gross_thrust_factor"] # this is an absolute hack to prevent spurious NaNs during the Newton solve - interior = (inputs['fltcond|rho']**2 * inputs['fltcond|Utrue']**2 + 2*inputs['fltcond|rho']*inputs['delta_p_hex'])/(1+static_pressure_loss_factor) - interior[np.where(interior<0.0)]=1e-10 - mdot = inputs['area_nozzle'] * np.sqrt(interior) - mdot[np.where(mdot<0.0)]=1e-10 + interior = ( + inputs["fltcond|rho"] ** 2 * inputs["fltcond|Utrue"] ** 2 + + 2 * inputs["fltcond|rho"] * inputs["delta_p_hex"] + ) / (1 + static_pressure_loss_factor) + interior[np.where(interior < 0.0)] = 1e-10 + mdot = inputs["area_nozzle"] * np.sqrt(interior) + mdot[np.where(mdot < 0.0)] = 1e-10 # if self.pathname.split('.')[1] == 'climb': # print('Nozzle area:'+str(inputs['area_nozzle'])) # print('mdot:'+str(mdot)) # print('delta_p_hex:'+str(inputs['delta_p_hex'])) - outputs['mdot'] = mdot - outputs['drag'] = mdot * (inputs['fltcond|Utrue'] - cfg*mdot/inputs['area_nozzle']/inputs['fltcond|rho']) + outputs["mdot"] = mdot + outputs["drag"] = mdot * (inputs["fltcond|Utrue"] - cfg * mdot / inputs["area_nozzle"] / inputs["fltcond|rho"]) # def compute_partials(self, inputs, J): # static_pressure_loss_factor = self.options['static_pressure_loss_factor'] @@ -100,6 +104,7 @@ def compute(self, inputs, outputs): # J['drag','delta_p_hex'] = (inputs['fltcond|Utrue'] - 2*mdot/inputs['area_nozzle']/inputs['fltcond|rho']) * dmdotddeltap # J['drag','area_nozzle'] = (inputs['fltcond|Utrue'] - 2*mdot/inputs['area_nozzle']/inputs['fltcond|rho']) * dmdotdA + mdot * (mdot/inputs['area_nozzle']**2/inputs['fltcond|rho']) + class TemperatureIsentropic(ExplicitComponent): """ Compute static temperature via isentropic relation @@ -125,28 +130,29 @@ class TemperatureIsentropic(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('gamma', default=1.4, desc='Specific heat ratio') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("gamma", default=1.4, desc="Specific heat ratio") def setup(self): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - self.add_input('Tt', shape=(nn,), units='K') - self.add_input('M', shape=(nn,)) - self.add_output('T', shape=(nn,), units='K') + nn = self.options["num_nodes"] + gam = self.options["gamma"] + self.add_input("Tt", shape=(nn,), units="K") + self.add_input("M", shape=(nn,)) + self.add_output("T", shape=(nn,), units="K") arange = np.arange(nn) - self.declare_partials(['T'], ['M','Tt'], rows=arange, cols=arange) + self.declare_partials(["T"], ["M", "Tt"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - outputs['T'] = inputs['Tt'] * (1 + (gam-1)/2 * inputs['M']**2) ** -1 + nn = self.options["num_nodes"] + gam = self.options["gamma"] + outputs["T"] = inputs["Tt"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** -1 def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - J['T','Tt'] = (1 + (gam-1)/2 * inputs['M']**2) ** -1 - J['T','M'] = - inputs['Tt'] * (1 + (gam-1)/2 * inputs['M']**2) ** -2 * (gam-1) * inputs['M'] + nn = self.options["num_nodes"] + gam = self.options["gamma"] + J["T", "Tt"] = (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** -1 + J["T", "M"] = -inputs["Tt"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** -2 * (gam - 1) * inputs["M"] + class TotalTemperatureIsentropic(ExplicitComponent): """ @@ -173,28 +179,29 @@ class TotalTemperatureIsentropic(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('gamma', default=1.4, desc='Specific heat ratio') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("gamma", default=1.4, desc="Specific heat ratio") def setup(self): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - self.add_input('T', shape=(nn,), units='K') - self.add_input('M', shape=(nn,)) - self.add_output('Tt', shape=(nn,), units='K') + nn = self.options["num_nodes"] + gam = self.options["gamma"] + self.add_input("T", shape=(nn,), units="K") + self.add_input("M", shape=(nn,)) + self.add_output("Tt", shape=(nn,), units="K") arange = np.arange(nn) - self.declare_partials(['Tt'], ['T','M'], rows=arange, cols=arange) + self.declare_partials(["Tt"], ["T", "M"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - outputs['Tt'] = inputs['T'] * (1 + (gam-1)/2 * inputs['M']**2) + nn = self.options["num_nodes"] + gam = self.options["gamma"] + outputs["Tt"] = inputs["T"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - J['Tt','T'] = 1 + (gam-1)/2 * inputs['M']**2 - J['Tt','M'] = inputs['T'] * (gam-1) * inputs['M'] + nn = self.options["num_nodes"] + gam = self.options["gamma"] + J["Tt", "T"] = 1 + (gam - 1) / 2 * inputs["M"] ** 2 + J["Tt", "M"] = inputs["T"] * (gam - 1) * inputs["M"] + class PressureIsentropic(ExplicitComponent): """ @@ -221,21 +228,22 @@ class PressureIsentropic(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('gamma', default=1.4, desc='Specific heat ratio') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("gamma", default=1.4, desc="Specific heat ratio") def setup(self): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - self.add_input('pt', shape=(nn,), units='Pa') - self.add_input('M', shape=(nn,)) - self.add_output('p', shape=(nn,), units='Pa') - self.declare_partials(['*'], ['*'], method='cs') + nn = self.options["num_nodes"] + gam = self.options["gamma"] + self.add_input("pt", shape=(nn,), units="Pa") + self.add_input("M", shape=(nn,)) + self.add_output("p", shape=(nn,), units="Pa") + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - outputs['p'] = inputs['pt'] * (1 + (gam-1)/2 * inputs['M']**2) ** (- gam / (gam - 1)) + nn = self.options["num_nodes"] + gam = self.options["gamma"] + outputs["p"] = inputs["pt"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** (-gam / (gam - 1)) + class TotalPressureIsentropic(ExplicitComponent): """ @@ -262,28 +270,35 @@ class TotalPressureIsentropic(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('gamma', default=1.4, desc='Specific heat ratio') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("gamma", default=1.4, desc="Specific heat ratio") def setup(self): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - self.add_input('p', shape=(nn,), units='Pa') - self.add_input('M', shape=(nn,)) - self.add_output('pt', shape=(nn,), units='Pa') + nn = self.options["num_nodes"] + gam = self.options["gamma"] + self.add_input("p", shape=(nn,), units="Pa") + self.add_input("M", shape=(nn,)) + self.add_output("pt", shape=(nn,), units="Pa") arange = np.arange(nn) - self.declare_partials(['pt'], ['p','M'], rows=arange, cols=arange) + self.declare_partials(["pt"], ["p", "M"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - outputs['pt'] = inputs['p'] * (1 + (gam-1)/2 * inputs['M']**2) ** (gam / (gam - 1)) + nn = self.options["num_nodes"] + gam = self.options["gamma"] + outputs["pt"] = inputs["p"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** (gam / (gam - 1)) def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - J['pt','p'] = (1 + (gam-1)/2 * inputs['M']**2) ** ( gam / (gam - 1)) - J['pt','M'] = inputs['p'] * (gam-1) * inputs['M'] *(gam / (gam - 1)) * (1 + (gam-1)/2 * inputs['M']**2) ** ( gam / (gam - 1) - 1) + nn = self.options["num_nodes"] + gam = self.options["gamma"] + J["pt", "p"] = (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** (gam / (gam - 1)) + J["pt", "M"] = ( + inputs["p"] + * (gam - 1) + * inputs["M"] + * (gam / (gam - 1)) + * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** (gam / (gam - 1) - 1) + ) + class DensityIdealGas(ExplicitComponent): """ @@ -311,21 +326,22 @@ class DensityIdealGas(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('R', default=287.05, desc='Gas constant') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("R", default=287.05, desc="Gas constant") def setup(self): - nn = self.options['num_nodes'] - R = self.options['R'] - self.add_input('p', shape=(nn,), units='Pa') - self.add_input('T', shape=(nn,), units='K') - self.add_output('rho', shape=(nn,), units='kg/m**3') - self.declare_partials(['*'], ['*'], method='cs') + nn = self.options["num_nodes"] + R = self.options["R"] + self.add_input("p", shape=(nn,), units="Pa") + self.add_input("T", shape=(nn,), units="K") + self.add_output("rho", shape=(nn,), units="kg/m**3") + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - R = self.options['R'] - outputs['rho'] = inputs['p'] / R / inputs['T'] + nn = self.options["num_nodes"] + R = self.options["R"] + outputs["rho"] = inputs["p"] / R / inputs["T"] + class SpeedOfSound(ExplicitComponent): """ @@ -352,31 +368,32 @@ class SpeedOfSound(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('R', default=287.05, desc='Gas constant') - self.options.declare('gamma', default=1.4, desc='Specific heat ratio') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("R", default=287.05, desc="Gas constant") + self.options.declare("gamma", default=1.4, desc="Specific heat ratio") def setup(self): - nn = self.options['num_nodes'] - self.add_input('T', shape=(nn,), units='K') - self.add_output('a', shape=(nn,), units='m/s', lower=1e0) + nn = self.options["num_nodes"] + self.add_input("T", shape=(nn,), units="K") + self.add_output("a", shape=(nn,), units="m/s", lower=1e0) arange = np.arange(nn) - self.declare_partials(['a'], ['T'], rows=arange, cols=arange) + self.declare_partials(["a"], ["T"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - R = self.options['R'] - gam = self.options['gamma'] - T = inputs['T'].copy() - T[np.where(T<0.0)]=100 - outputs['a'] = np.sqrt(gam * R * T) + nn = self.options["num_nodes"] + R = self.options["R"] + gam = self.options["gamma"] + T = inputs["T"].copy() + T[np.where(T < 0.0)] = 100 + outputs["a"] = np.sqrt(gam * R * T) def compute_partials(self, inputs, J): - R = self.options['R'] - gam = self.options['gamma'] - T = inputs['T'].copy() - T[np.where(T<0.0)]=100 - J['a', 'T'] = 0.5 * np.sqrt(gam * R) / np.sqrt(T) + R = self.options["R"] + gam = self.options["gamma"] + T = inputs["T"].copy() + T[np.where(T < 0.0)] = 100 + J["a", "T"] = 0.5 * np.sqrt(gam * R) / np.sqrt(T) + class MachNumberfromSpeed(ExplicitComponent): """ @@ -401,18 +418,19 @@ class MachNumberfromSpeed(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('a', shape=(nn,), units='m/s') - self.add_input('Utrue', shape=(nn,), units='m/s') - self.add_output('M', shape=(nn,)) - self.declare_partials(['*'], ['*'], method='cs') + nn = self.options["num_nodes"] + self.add_input("a", shape=(nn,), units="m/s") + self.add_input("Utrue", shape=(nn,), units="m/s") + self.add_output("M", shape=(nn,)) + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - outputs['M'] = inputs['Utrue'] / inputs['a'] + nn = self.options["num_nodes"] + outputs["M"] = inputs["Utrue"] / inputs["a"] + class HeatAdditionPressureLoss(ExplicitComponent): """ @@ -449,66 +467,87 @@ class HeatAdditionPressureLoss(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('Tt_in', shape=(nn,), units='K') - self.add_input('pt_in', shape=(nn,), units='Pa') - self.add_input('mdot', shape=(nn,), units='kg/s') - self.add_input('rho', shape=(nn,), units='kg/m**3') - self.add_input('area', units='m**2') - self.add_input('delta_p', shape=(nn,), val=0.0, units='Pa') - self.add_input('dynamic_pressure_loss_factor', val=0.0) - self.add_input('pressure_recovery', shape=(nn,), val=np.ones((nn,))) - self.add_input('heat_in', shape=(nn,), val=0.0, units='W') - self.add_input('cp', units='J/kg/K') - - self.add_output('Tt_out', shape=(nn,), units='K') - self.add_output('pt_out', shape=(nn,), units='Pa') + nn = self.options["num_nodes"] + self.add_input("Tt_in", shape=(nn,), units="K") + self.add_input("pt_in", shape=(nn,), units="Pa") + self.add_input("mdot", shape=(nn,), units="kg/s") + self.add_input("rho", shape=(nn,), units="kg/m**3") + self.add_input("area", units="m**2") + self.add_input("delta_p", shape=(nn,), val=0.0, units="Pa") + self.add_input("dynamic_pressure_loss_factor", val=0.0) + self.add_input("pressure_recovery", shape=(nn,), val=np.ones((nn,))) + self.add_input("heat_in", shape=(nn,), val=0.0, units="W") + self.add_input("cp", units="J/kg/K") + + self.add_output("Tt_out", shape=(nn,), units="K") + self.add_output("pt_out", shape=(nn,), units="Pa") arange = np.arange(nn) - self.declare_partials(['Tt_out'], ['Tt_in','heat_in','mdot'], rows=arange, cols=arange) - self.declare_partials(['Tt_out'], ['cp'], rows=arange, cols=np.zeros((nn,))) - self.declare_partials(['pt_out'], ['pt_in','pressure_recovery','delta_p', 'rho','mdot'], rows=arange, cols=arange) - self.declare_partials(['pt_out'], ['area','dynamic_pressure_loss_factor'], rows=arange, cols=np.zeros((nn,))) + self.declare_partials(["Tt_out"], ["Tt_in", "heat_in", "mdot"], rows=arange, cols=arange) + self.declare_partials(["Tt_out"], ["cp"], rows=arange, cols=np.zeros((nn,))) + self.declare_partials( + ["pt_out"], ["pt_in", "pressure_recovery", "delta_p", "rho", "mdot"], rows=arange, cols=arange + ) + self.declare_partials(["pt_out"], ["area", "dynamic_pressure_loss_factor"], rows=arange, cols=np.zeros((nn,))) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - dynamic_pressure = 0.5 * inputs['mdot'] ** 2 / inputs['rho'] / inputs['area'] ** 2 - - if np.min(inputs['mdot']) <= 0.0: - raise ValueError(self.msginfo, inputs['mdot']) - tt_out = inputs['Tt_in'] + inputs['heat_in'] / inputs['cp'] / inputs['mdot'] - pt_out = inputs['pt_in'] * inputs['pressure_recovery'] - dynamic_pressure * inputs['dynamic_pressure_loss_factor'] + inputs['delta_p'] + nn = self.options["num_nodes"] + dynamic_pressure = 0.5 * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 2 + + if np.min(inputs["mdot"]) <= 0.0: + raise ValueError(self.msginfo, inputs["mdot"]) + tt_out = inputs["Tt_in"] + inputs["heat_in"] / inputs["cp"] / inputs["mdot"] + pt_out = ( + inputs["pt_in"] * inputs["pressure_recovery"] + - dynamic_pressure * inputs["dynamic_pressure_loss_factor"] + + inputs["delta_p"] + ) # outputs['Tt_out'] = inputs['Tt_in'] + inputs['heat_in'] / inputs['cp'] / inputs['mdot'] # outputs['Tt_out'] = np.where(tt_out <= 0.0, inputs['Tt_in'], tt_out) # outputs['pt_out'] = np.where(pt_out <= 0.0, inputs['pt_in'] * inputs['pressure_recovery'], pt_out) - outputs['pt_out'] = pt_out - if np.max(pt_out)>1e8: - raise ValueError(self.msginfo, inputs['pt_in'], inputs['rho'], inputs['delta_p']) - outputs['Tt_out'] = tt_out + outputs["pt_out"] = pt_out + if np.max(pt_out) > 1e8: + raise ValueError(self.msginfo, inputs["pt_in"], inputs["rho"], inputs["delta_p"]) + outputs["Tt_out"] = tt_out def compute_partials(self, inputs, J): - dynamic_pressure = 0.5 * inputs['mdot'] ** 2 / inputs['rho'] / inputs['area'] ** 2 - tt_out = inputs['Tt_in'] + inputs['heat_in'] / inputs['cp'] / inputs['mdot'] + dynamic_pressure = 0.5 * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 2 + tt_out = inputs["Tt_in"] + inputs["heat_in"] / inputs["cp"] / inputs["mdot"] # bool_array_tt = np.where(tt_out <= 0.0, np.zeros(inputs['Tt_in'].shape), np.ones(inputs['Tt_in'].shape)) - pt_out = inputs['pt_in'] * inputs['pressure_recovery'] - dynamic_pressure * inputs['dynamic_pressure_loss_factor'] + inputs['delta_p'] + pt_out = ( + inputs["pt_in"] * inputs["pressure_recovery"] + - dynamic_pressure * inputs["dynamic_pressure_loss_factor"] + + inputs["delta_p"] + ) # bool_array_pt = np.where(pt_out <= 0.0, np.zeros(inputs['pt_in'].shape), np.ones(inputs['pt_in'].shape)) - nn = self.options['num_nodes'] - J['Tt_out','Tt_in'] = np.ones((nn,)) - J['Tt_out','heat_in'] = 1 / inputs['cp']/inputs['mdot'] - J['Tt_out','cp'] = - inputs['heat_in'] / inputs['cp'] ** 2 / inputs['mdot'] - J['Tt_out','mdot'] = - inputs['heat_in'] / inputs['cp'] / inputs['mdot'] ** 2 - - J['pt_out', 'pt_in'] = inputs['pressure_recovery'] - J['pt_out', 'pressure_recovery'] = inputs['pt_in'] - J['pt_out', 'delta_p'] = np.ones((nn,)) - J['pt_out', 'mdot'] = - inputs['dynamic_pressure_loss_factor'] * inputs['mdot'] / inputs['rho'] / inputs['area'] ** 2 - J['pt_out', 'rho'] = 0.5 * inputs['dynamic_pressure_loss_factor'] * inputs['mdot'] ** 2 / inputs['rho'] ** 2 / inputs['area'] ** 2 - J['pt_out', 'area'] = inputs['dynamic_pressure_loss_factor'] * inputs['mdot'] ** 2 / inputs['rho'] / inputs['area'] ** 3 - J['pt_out', 'dynamic_pressure_loss_factor'] = - 0.5 * inputs['mdot'] ** 2 / inputs['rho'] / inputs['area'] ** 2 + nn = self.options["num_nodes"] + J["Tt_out", "Tt_in"] = np.ones((nn,)) + J["Tt_out", "heat_in"] = 1 / inputs["cp"] / inputs["mdot"] + J["Tt_out", "cp"] = -inputs["heat_in"] / inputs["cp"] ** 2 / inputs["mdot"] + J["Tt_out", "mdot"] = -inputs["heat_in"] / inputs["cp"] / inputs["mdot"] ** 2 + + J["pt_out", "pt_in"] = inputs["pressure_recovery"] + J["pt_out", "pressure_recovery"] = inputs["pt_in"] + J["pt_out", "delta_p"] = np.ones((nn,)) + J["pt_out", "mdot"] = ( + -inputs["dynamic_pressure_loss_factor"] * inputs["mdot"] / inputs["rho"] / inputs["area"] ** 2 + ) + J["pt_out", "rho"] = ( + 0.5 + * inputs["dynamic_pressure_loss_factor"] + * inputs["mdot"] ** 2 + / inputs["rho"] ** 2 + / inputs["area"] ** 2 + ) + J["pt_out", "area"] = ( + inputs["dynamic_pressure_loss_factor"] * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 3 + ) + J["pt_out", "dynamic_pressure_loss_factor"] = -0.5 * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 2 + class MassFlow(ExplicitComponent): """ @@ -536,53 +575,56 @@ class MassFlow(ExplicitComponent): num_nodes : int Number of analysis points to run (scalar, dimensionless) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('a', shape=(nn,), units='m/s') - self.add_input('area', shape=(nn,), units='m**2') - self.add_input('rho', shape=(nn,), units='kg/m**3') - self.add_input('M', shape=(nn,)) - self.add_output('mdot', shape=(nn,), units='kg/s', lower=1e-5) + nn = self.options["num_nodes"] + self.add_input("a", shape=(nn,), units="m/s") + self.add_input("area", shape=(nn,), units="m**2") + self.add_input("rho", shape=(nn,), units="kg/m**3") + self.add_input("M", shape=(nn,)) + self.add_output("mdot", shape=(nn,), units="kg/s", lower=1e-5) arange = np.arange(0, nn) - self.declare_partials(['mdot'], ['M', 'a', 'rho', 'area'], rows=arange, cols=arange) + self.declare_partials(["mdot"], ["M", "a", "rho", "area"], rows=arange, cols=arange) def compute(self, inputs, outputs): - outputs['mdot'] = inputs['M'] * inputs['a'] * inputs['area'] * inputs['rho'] + outputs["mdot"] = inputs["M"] * inputs["a"] * inputs["area"] * inputs["rho"] def compute_partials(self, inputs, J): - J['mdot','M'] = inputs['a'] * inputs['area'] * inputs['rho'] - J['mdot','a'] = inputs['M'] * inputs['area'] * inputs['rho'] - J['mdot','area'] = inputs['M'] * inputs['a'] * inputs['rho'] - J['mdot','rho'] = inputs['M'] * inputs['a'] * inputs['area'] + J["mdot", "M"] = inputs["a"] * inputs["area"] * inputs["rho"] + J["mdot", "a"] = inputs["M"] * inputs["area"] * inputs["rho"] + J["mdot", "area"] = inputs["M"] * inputs["a"] * inputs["rho"] + J["mdot", "rho"] = inputs["M"] * inputs["a"] * inputs["area"] + class MachNumberDuct(ImplicitComponent): def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('mdot', shape=(nn,), units='kg/s') - self.add_input('a', shape=(nn,), units='m/s') - self.add_input('area', units='m**2') - self.add_input('rho', shape=(nn,), units='kg/m**3') - # self.add_output('M', shape=(nn,), lower=0.0, upper=1.0) - self.add_output('M', shape=(nn,), val=np.ones((nn,))*0.6, lower=0.00001, upper=0.99999) + nn = self.options["num_nodes"] + self.add_input("mdot", shape=(nn,), units="kg/s") + self.add_input("a", shape=(nn,), units="m/s") + self.add_input("area", units="m**2") + self.add_input("rho", shape=(nn,), units="kg/m**3") + # self.add_output('M', shape=(nn,), lower=0.0, upper=1.0) + self.add_output("M", shape=(nn,), val=np.ones((nn,)) * 0.6, lower=0.00001, upper=0.99999) arange = np.arange(0, nn) - self.declare_partials(['M'], ['mdot'], rows=arange, cols=arange, val=np.ones((nn, ))) - self.declare_partials(['M'], ['M', 'a', 'rho'], rows=arange, cols=arange) - self.declare_partials(['M'], ['area'], rows=arange, cols=np.zeros((nn, ), dtype=np.int32)) + self.declare_partials(["M"], ["mdot"], rows=arange, cols=arange, val=np.ones((nn,))) + self.declare_partials(["M"], ["M", "a", "rho"], rows=arange, cols=arange) + self.declare_partials(["M"], ["area"], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) def apply_nonlinear(self, inputs, outputs, residuals): - residuals['M'] = inputs['mdot'] - outputs['M'] * inputs['a'] * inputs['area'] * inputs['rho'] + residuals["M"] = inputs["mdot"] - outputs["M"] * inputs["a"] * inputs["area"] * inputs["rho"] def linearize(self, inputs, outputs, J): - J['M','M'] = - inputs['a'] * inputs['area'] * inputs['rho'] - J['M','a'] = - outputs['M'] * inputs['area'] * inputs['rho'] - J['M','area'] = - outputs['M'] * inputs['a'] * inputs['rho'] - J['M','rho'] = - outputs['M'] * inputs['a'] * inputs['area'] + J["M", "M"] = -inputs["a"] * inputs["area"] * inputs["rho"] + J["M", "a"] = -outputs["M"] * inputs["area"] * inputs["rho"] + J["M", "area"] = -outputs["M"] * inputs["a"] * inputs["rho"] + J["M", "rho"] = -outputs["M"] * inputs["a"] * inputs["area"] + class DuctExitPressureRatioImplicit(ImplicitComponent): """ @@ -607,23 +649,26 @@ class DuctExitPressureRatioImplicit(ImplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('p_exit', shape=(nn,), units='Pa') - self.add_input('pt', shape=(nn,), units='Pa') - self.add_output('nozzle_pressure_ratio', shape=(nn,), val=np.ones((nn,))*0.9, lower=0.01, upper=0.99999999999) + nn = self.options["num_nodes"] + self.add_input("p_exit", shape=(nn,), units="Pa") + self.add_input("pt", shape=(nn,), units="Pa") + self.add_output("nozzle_pressure_ratio", shape=(nn,), val=np.ones((nn,)) * 0.9, lower=0.01, upper=0.99999999999) arange = np.arange(0, nn) - self.declare_partials(['nozzle_pressure_ratio'], ['nozzle_pressure_ratio'], rows=arange, cols=arange, val=np.ones((nn, ))) - self.declare_partials(['nozzle_pressure_ratio'], ['p_exit','pt'], rows=arange, cols=arange) + self.declare_partials( + ["nozzle_pressure_ratio"], ["nozzle_pressure_ratio"], rows=arange, cols=arange, val=np.ones((nn,)) + ) + self.declare_partials(["nozzle_pressure_ratio"], ["p_exit", "pt"], rows=arange, cols=arange) def apply_nonlinear(self, inputs, outputs, residuals): - residuals['nozzle_pressure_ratio'] = outputs['nozzle_pressure_ratio'] - inputs['p_exit'] / inputs['pt'] + residuals["nozzle_pressure_ratio"] = outputs["nozzle_pressure_ratio"] - inputs["p_exit"] / inputs["pt"] def linearize(self, inputs, outputs, J): - J['nozzle_pressure_ratio', 'p_exit'] = - 1 / inputs['pt'] - J['nozzle_pressure_ratio', 'pt'] = inputs['p_exit'] / inputs['pt'] ** 2 + J["nozzle_pressure_ratio", "p_exit"] = -1 / inputs["pt"] + J["nozzle_pressure_ratio", "pt"] = inputs["p_exit"] / inputs["pt"] ** 2 + class DuctExitMachNumber(ExplicitComponent): """ @@ -648,23 +693,28 @@ class DuctExitMachNumber(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('gamma', default=1.4, desc='Specific heat ratio') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("gamma", default=1.4, desc="Specific heat ratio") def setup(self): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - self.add_input('nozzle_pressure_ratio', shape=(nn,)) - self.add_output('M', shape=(nn,)) - self.declare_partials(['*'], ['*'], method='cs') + nn = self.options["num_nodes"] + gam = self.options["gamma"] + self.add_input("nozzle_pressure_ratio", shape=(nn,)) + self.add_output("M", shape=(nn,)) + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - gam = self.options['gamma'] - critical_pressure_ratio = (2/(gam+1))**(gam/(gam-1)) - if np.min(inputs['nozzle_pressure_ratio']) <= 0.0: + nn = self.options["num_nodes"] + gam = self.options["gamma"] + critical_pressure_ratio = (2 / (gam + 1)) ** (gam / (gam - 1)) + if np.min(inputs["nozzle_pressure_ratio"]) <= 0.0: raise ValueError(self.msginfo) - outputs['M'] = np.where(np.less_equal(inputs['nozzle_pressure_ratio'], critical_pressure_ratio), np.ones((nn,)), np.sqrt(((inputs['nozzle_pressure_ratio'])**((1-gam)/gam)-1)*2/(gam-1))) + outputs["M"] = np.where( + np.less_equal(inputs["nozzle_pressure_ratio"], critical_pressure_ratio), + np.ones((nn,)), + np.sqrt(((inputs["nozzle_pressure_ratio"]) ** ((1 - gam) / gam) - 1) * 2 / (gam - 1)), + ) + class NetForce(ExplicitComponent): """ @@ -697,26 +747,28 @@ class NetForce(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('cfg', default=0.98, desc='Factor on gross thrust (accounts for some duct losses)') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("cfg", default=0.98, desc="Factor on gross thrust (accounts for some duct losses)") def setup(self): - nn = self.options['num_nodes'] - self.add_input('mdot', shape=(nn,), units='kg/s') - self.add_input('Utrue_inf', shape=(nn,), units='m/s') - self.add_input('p_inf', shape=(nn,), units='Pa') - self.add_input('area_nozzle', shape=(nn,), units='m**2') - self.add_input('p_nozzle', shape=(nn,), units='Pa') - self.add_input('rho_nozzle', shape=(nn,), units='kg/m**3') + nn = self.options["num_nodes"] + self.add_input("mdot", shape=(nn,), units="kg/s") + self.add_input("Utrue_inf", shape=(nn,), units="m/s") + self.add_input("p_inf", shape=(nn,), units="Pa") + self.add_input("area_nozzle", shape=(nn,), units="m**2") + self.add_input("p_nozzle", shape=(nn,), units="Pa") + self.add_input("rho_nozzle", shape=(nn,), units="kg/m**3") - self.add_output('F_net', shape=(nn,), units='N') - self.declare_partials(['*'], ['*'], method='cs') + self.add_output("F_net", shape=(nn,), units="N") + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - cfg = self.options['cfg'] - outputs['F_net'] = (inputs['mdot'] * (inputs['mdot'] / inputs['area_nozzle'] / inputs['rho_nozzle']*cfg - inputs['Utrue_inf']) + - inputs['area_nozzle']*cfg*(inputs['p_nozzle']-inputs['p_inf'])) + nn = self.options["num_nodes"] + cfg = self.options["cfg"] + outputs["F_net"] = inputs["mdot"] * ( + inputs["mdot"] / inputs["area_nozzle"] / inputs["rho_nozzle"] * cfg - inputs["Utrue_inf"] + ) + inputs["area_nozzle"] * cfg * (inputs["p_nozzle"] - inputs["p_inf"]) + class Inlet(Group): """This group takes in ambient flight conditions and computes total quantities for downstream use @@ -744,19 +796,35 @@ class Inlet(Group): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('speedsound',SpeedOfSound(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('mach',MachNumberfromSpeed(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('freestreamtotaltemperature',TotalTemperatureIsentropic(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('freestreamtotalpressure',TotalPressureIsentropic(num_nodes=nn), promotes_inputs=['*']) + nn = self.options["num_nodes"] + self.add_subsystem("speedsound", SpeedOfSound(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("mach", MachNumberfromSpeed(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "freestreamtotaltemperature", + TotalTemperatureIsentropic(num_nodes=nn), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem("freestreamtotalpressure", TotalPressureIsentropic(num_nodes=nn), promotes_inputs=["*"]) # self.add_subsystem('inlet_recovery', ExecComp('eta_ram=1.0 - 0.00*tanh(10*M)', has_diag_partials=True, eta_ram=np.ones((nn,)), M=0.1*np.ones((nn,))), promotes_inputs=['M']) - self.add_subsystem('totalpressure', ExecComp('pt=pt_in * eta_ram', pt={'units':'Pa','val':np.ones((nn,)),'lower':1.0}, pt_in={'units':'Pa','val':np.ones((nn,))}, eta_ram=np.ones((nn,)), has_diag_partials=True), promotes_outputs=['pt']) - self.connect('freestreamtotalpressure.pt','totalpressure.pt_in') + self.add_subsystem( + "totalpressure", + ExecComp( + "pt=pt_in * eta_ram", + pt={"units": "Pa", "val": np.ones((nn,)), "lower": 1.0}, + pt_in={"units": "Pa", "val": np.ones((nn,))}, + eta_ram=np.ones((nn,)), + has_diag_partials=True, + ), + promotes_outputs=["pt"], + ) + self.connect("freestreamtotalpressure.pt", "totalpressure.pt_in") # self.connect('inlet_recovery.eta_ram','totalpressure.eta_ram') + class DuctStation(Group): """A 'normal' station in a duct flow. @@ -787,18 +855,21 @@ class DuctStation(Group): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('totals', HeatAdditionPressureLoss(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('temp', TemperatureIsentropic(num_nodes=nn), promotes_inputs=['M'], promotes_outputs=['*']) - self.connect('Tt_out','temp.Tt') - self.add_subsystem('pressure', PressureIsentropic(num_nodes=nn), promotes_inputs=['M'], promotes_outputs=['*']) - self.connect('pt_out','pressure.pt') - self.add_subsystem('density', DensityIdealGas(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('speedsound', SpeedOfSound(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('mach', MachNumberDuct(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + nn = self.options["num_nodes"] + self.add_subsystem( + "totals", HeatAdditionPressureLoss(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("temp", TemperatureIsentropic(num_nodes=nn), promotes_inputs=["M"], promotes_outputs=["*"]) + self.connect("Tt_out", "temp.Tt") + self.add_subsystem("pressure", PressureIsentropic(num_nodes=nn), promotes_inputs=["M"], promotes_outputs=["*"]) + self.connect("pt_out", "pressure.pt") + self.add_subsystem("density", DensityIdealGas(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("speedsound", SpeedOfSound(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("mach", MachNumberDuct(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + class NozzlePressureLoss(ExplicitComponent): """This group adds proportional pressure loss to the nozzle component @@ -813,7 +884,7 @@ class NozzlePressureLoss(ExplicitComponent): Nozzle cross sectional area (vector, m**2) dynamic_pressure_loss_factor : float Total pressure loss as a fraction of dynamic pressure - + Outputs ------- pt : float @@ -824,39 +895,49 @@ class NozzlePressureLoss(ExplicitComponent): num_nodes : int Number of conditions to analyze """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('pt_in', shape=(nn,), units='Pa') - self.add_input('mdot', shape=(nn,), units='kg/s') - self.add_input('rho', shape=(nn,), units='kg/m**3') - self.add_input('area', shape=(nn,), units='m**2') - self.add_input('dynamic_pressure_loss_factor', val=0.0) + nn = self.options["num_nodes"] + self.add_input("pt_in", shape=(nn,), units="Pa") + self.add_input("mdot", shape=(nn,), units="kg/s") + self.add_input("rho", shape=(nn,), units="kg/m**3") + self.add_input("area", shape=(nn,), units="m**2") + self.add_input("dynamic_pressure_loss_factor", val=0.0) - self.add_output('pt', shape=(nn,), units='Pa', lower=1.0) + self.add_output("pt", shape=(nn,), units="Pa", lower=1.0) arange = np.arange(nn) - self.declare_partials(['pt'], ['pt_in','rho','mdot','area'], rows=arange, cols=arange) - self.declare_partials(['pt'], ['dynamic_pressure_loss_factor'], rows=arange, cols=np.zeros((nn,))) + self.declare_partials(["pt"], ["pt_in", "rho", "mdot", "area"], rows=arange, cols=arange) + self.declare_partials(["pt"], ["dynamic_pressure_loss_factor"], rows=arange, cols=np.zeros((nn,))) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - dynamic_pressure = 0.5 * inputs['mdot'] ** 2 / inputs['rho'] / inputs['area'] ** 2 + nn = self.options["num_nodes"] + dynamic_pressure = 0.5 * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 2 - pt_out = inputs['pt_in'] - dynamic_pressure * inputs['dynamic_pressure_loss_factor'] - outputs['pt'] = pt_out + pt_out = inputs["pt_in"] - dynamic_pressure * inputs["dynamic_pressure_loss_factor"] + outputs["pt"] = pt_out def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] + + J["pt", "pt_in"] = np.ones((nn,)) + J["pt", "mdot"] = -inputs["dynamic_pressure_loss_factor"] * inputs["mdot"] / inputs["rho"] / inputs["area"] ** 2 + J["pt", "rho"] = ( + 0.5 + * inputs["dynamic_pressure_loss_factor"] + * inputs["mdot"] ** 2 + / inputs["rho"] ** 2 + / inputs["area"] ** 2 + ) + J["pt", "area"] = ( + inputs["dynamic_pressure_loss_factor"] * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 3 + ) + J["pt", "dynamic_pressure_loss_factor"] = -0.5 * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 2 - J['pt', 'pt_in'] = np.ones((nn,)) - J['pt', 'mdot'] = - inputs['dynamic_pressure_loss_factor'] * inputs['mdot'] / inputs['rho'] / inputs['area'] ** 2 - J['pt', 'rho'] = 0.5 * inputs['dynamic_pressure_loss_factor'] * inputs['mdot'] ** 2 / inputs['rho'] ** 2 / inputs['area'] ** 2 - J['pt', 'area'] = inputs['dynamic_pressure_loss_factor'] * inputs['mdot'] ** 2 / inputs['rho'] / inputs['area'] ** 3 - J['pt', 'dynamic_pressure_loss_factor'] = - 0.5 * inputs['mdot'] ** 2 / inputs['rho'] / inputs['area'] ** 2 class OutletNozzle(Group): """This group is designed to be the farthest downstream point in a ducted heat exchanger model. @@ -883,161 +964,223 @@ class OutletNozzle(Group): num_nodes : int Number of conditions to analyze """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('pressureloss', NozzlePressureLoss(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('pressureratio', DuctExitPressureRatioImplicit(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('machimplicit', DuctExitMachNumber(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('temp', TemperatureIsentropic(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('pressure', PressureIsentropic(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('density', DensityIdealGas(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('speedsound', SpeedOfSound(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('massflow', MassFlow(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=[('mdot','mdot_actual')]) + nn = self.options["num_nodes"] + self.add_subsystem( + "pressureloss", NozzlePressureLoss(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem( + "pressureratio", DuctExitPressureRatioImplicit(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem( + "machimplicit", DuctExitMachNumber(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("temp", TemperatureIsentropic(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("pressure", PressureIsentropic(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("density", DensityIdealGas(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("speedsound", SpeedOfSound(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "massflow", MassFlow(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=[("mdot", "mdot_actual")] + ) + class ImplicitCompressibleDuct(Group): """ Ducted heat exchanger with compressible flow assumptions """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points' ) + self.options.declare("num_nodes", default=1, desc="Number of analysis points") def setup(self): - nn = self.options['num_nodes'] - - iv = self.add_subsystem('dv', IndepVarComp(), promotes_outputs=['cp','*_1','*_2','*_3','*_nozzle','convergence_hack']) - iv.add_output('cp', val=1002.93, units='J/kg/K') - - iv.add_output('area_1', val=60, units='inch**2') - iv.add_output('delta_p_1', val=np.zeros((nn,)), units='Pa') - iv.add_output('heat_in_1', val=np.zeros((nn,)), units='W') - iv.add_output('pressure_recovery_1', val=np.ones((nn,))) - - iv.add_output('delta_p_2', val=np.ones((nn,))*0., units='Pa') - iv.add_output('heat_in_2', val=np.ones((nn,))*0., units='W') - iv.add_output('pressure_recovery_2', val=np.ones((nn,))) - - iv.add_output('pressure_recovery_3', val=np.ones((nn,))) - - iv.add_output('area_nozzle', val=58*np.ones((nn,)), units='inch**2') - iv.add_output('convergence_hack', val=-20, units='Pa') - - self.add_subsystem('inlet', Inlet(num_nodes=nn), - promotes_inputs=[('p','p_inf'),('T','T_inf'),'Utrue']) - - self.add_subsystem('sta1', DuctStation(num_nodes=nn), promotes_inputs=['mdot','cp', - ('area','area_1'), - ('delta_p','delta_p_1'), - ('heat_in','heat_in_1'), - ('pressure_recovery','pressure_recovery_1')]) - self.connect('inlet.pt','sta1.pt_in') - self.connect('inlet.Tt','sta1.Tt_in') - - - self.add_subsystem('sta2', DuctStation(num_nodes=nn), promotes_inputs=['mdot','cp', - ('area','area_2'), - ('delta_p','delta_p_2'), - ('heat_in','heat_in_2'), - ('pressure_recovery','pressure_recovery_2')]) - self.connect('sta1.pt_out','sta2.pt_in') - self.connect('sta1.Tt_out','sta2.Tt_in') - - self.add_subsystem('hx', HXGroup(num_nodes=nn), promotes_inputs=[('mdot_cold','mdot'),'mdot_hot','T_in_hot','rho_hot','ac|propulsion|thermal|hx|n_wide_cold'], - promotes_outputs=['T_out_hot']) - self.connect('sta2.T','hx.T_in_cold') - self.connect('sta2.rho','hx.rho_cold') - - self.add_subsystem('sta3', DuctStation(num_nodes=nn), promotes_inputs=['mdot','cp', - ('pressure_recovery','pressure_recovery_3'), - ('area','area_3')]) - self.connect('sta2.pt_out','sta3.pt_in') - self.connect('sta2.Tt_out','sta3.Tt_in') - self.connect('hx.delta_p_cold','sta3.delta_p') - self.connect('hx.heat_transfer','sta3.heat_in') - self.connect('hx.frontal_area',['area_2','area_3']) - self.add_subsystem('pexit',AddSubtractComp(output_name='p_exit',input_names=['p_inf','convergence_hack'],vec_size=[nn,1],units='Pa'),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('nozzle', OutletNozzle(num_nodes=nn), - promotes_inputs=['p_exit',('area','area_nozzle')], - promotes_outputs=['mdot']) - self.connect('sta3.pt_out','nozzle.pt') - self.connect('sta3.Tt_out','nozzle.Tt') - - self.add_subsystem('force', NetForce(num_nodes=nn), promotes_inputs=['mdot','p_inf',('Utrue_inf','Utrue'),'area_nozzle']) - self.connect('nozzle.p','force.p_nozzle') - self.connect('nozzle.rho','force.rho_nozzle') + nn = self.options["num_nodes"] + + iv = self.add_subsystem( + "dv", IndepVarComp(), promotes_outputs=["cp", "*_1", "*_2", "*_3", "*_nozzle", "convergence_hack"] + ) + iv.add_output("cp", val=1002.93, units="J/kg/K") + + iv.add_output("area_1", val=60, units="inch**2") + iv.add_output("delta_p_1", val=np.zeros((nn,)), units="Pa") + iv.add_output("heat_in_1", val=np.zeros((nn,)), units="W") + iv.add_output("pressure_recovery_1", val=np.ones((nn,))) + + iv.add_output("delta_p_2", val=np.ones((nn,)) * 0.0, units="Pa") + iv.add_output("heat_in_2", val=np.ones((nn,)) * 0.0, units="W") + iv.add_output("pressure_recovery_2", val=np.ones((nn,))) + + iv.add_output("pressure_recovery_3", val=np.ones((nn,))) + + iv.add_output("area_nozzle", val=58 * np.ones((nn,)), units="inch**2") + iv.add_output("convergence_hack", val=-20, units="Pa") + + self.add_subsystem("inlet", Inlet(num_nodes=nn), promotes_inputs=[("p", "p_inf"), ("T", "T_inf"), "Utrue"]) + + self.add_subsystem( + "sta1", + DuctStation(num_nodes=nn), + promotes_inputs=[ + "mdot", + "cp", + ("area", "area_1"), + ("delta_p", "delta_p_1"), + ("heat_in", "heat_in_1"), + ("pressure_recovery", "pressure_recovery_1"), + ], + ) + self.connect("inlet.pt", "sta1.pt_in") + self.connect("inlet.Tt", "sta1.Tt_in") + + self.add_subsystem( + "sta2", + DuctStation(num_nodes=nn), + promotes_inputs=[ + "mdot", + "cp", + ("area", "area_2"), + ("delta_p", "delta_p_2"), + ("heat_in", "heat_in_2"), + ("pressure_recovery", "pressure_recovery_2"), + ], + ) + self.connect("sta1.pt_out", "sta2.pt_in") + self.connect("sta1.Tt_out", "sta2.Tt_in") + + self.add_subsystem( + "hx", + HXGroup(num_nodes=nn), + promotes_inputs=[ + ("mdot_cold", "mdot"), + "mdot_hot", + "T_in_hot", + "rho_hot", + "ac|propulsion|thermal|hx|n_wide_cold", + ], + promotes_outputs=["T_out_hot"], + ) + self.connect("sta2.T", "hx.T_in_cold") + self.connect("sta2.rho", "hx.rho_cold") + + self.add_subsystem( + "sta3", + DuctStation(num_nodes=nn), + promotes_inputs=["mdot", "cp", ("pressure_recovery", "pressure_recovery_3"), ("area", "area_3")], + ) + self.connect("sta2.pt_out", "sta3.pt_in") + self.connect("sta2.Tt_out", "sta3.Tt_in") + self.connect("hx.delta_p_cold", "sta3.delta_p") + self.connect("hx.heat_transfer", "sta3.heat_in") + self.connect("hx.frontal_area", ["area_2", "area_3"]) + self.add_subsystem( + "pexit", + AddSubtractComp( + output_name="p_exit", input_names=["p_inf", "convergence_hack"], vec_size=[nn, 1], units="Pa" + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "nozzle", + OutletNozzle(num_nodes=nn), + promotes_inputs=["p_exit", ("area", "area_nozzle")], + promotes_outputs=["mdot"], + ) + self.connect("sta3.pt_out", "nozzle.pt") + self.connect("sta3.Tt_out", "nozzle.Tt") + + self.add_subsystem( + "force", NetForce(num_nodes=nn), promotes_inputs=["mdot", "p_inf", ("Utrue_inf", "Utrue"), "area_nozzle"] + ) + self.connect("nozzle.p", "force.p_nozzle") + self.connect("nozzle.rho", "force.rho_nozzle") + class FlowMatcher(ImplicitComponent): - def initialize(self): - self.options.declare('num_nodes', default=1) - + self.options.declare("num_nodes", default=1) + def setup(self): - nn = self.options['num_nodes'] - self.add_input('mdot_actual', shape=(nn,), units='kg/s') - self.add_output('mdot', shape=(nn,), units='kg/s', lower=0.005, upper=15.0, val=9.0) + nn = self.options["num_nodes"] + self.add_input("mdot_actual", shape=(nn,), units="kg/s") + self.add_output("mdot", shape=(nn,), units="kg/s", lower=0.005, upper=15.0, val=9.0) arange = np.arange(0, nn) - self.declare_partials(['mdot'], ['mdot_actual'], rows=arange, cols=arange, val=np.ones((nn, ))) - self.declare_partials(['mdot'], ['mdot'], rows=arange, cols=arange, val=-np.ones((nn, ))) + self.declare_partials(["mdot"], ["mdot_actual"], rows=arange, cols=arange, val=np.ones((nn,))) + self.declare_partials(["mdot"], ["mdot"], rows=arange, cols=arange, val=-np.ones((nn,))) def apply_nonlinear(self, inputs, outputs, residuals): - residuals['mdot'] = inputs['mdot_actual'] - outputs['mdot'] + residuals["mdot"] = inputs["mdot_actual"] - outputs["mdot"] + class ImplicitCompressibleDuct_ExternalHX(Group): """ Ducted heat exchanger with compressible flow assumptions """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points' ) - self.options.declare('cfg', default=0.98, desc='Gross thrust coefficient') + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + self.options.declare("cfg", default=0.98, desc="Gross thrust coefficient") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] - iv = self.add_subsystem('dv', IndepVarComp(), promotes_outputs=['cp','*_1','*_2','*_3','convergence_hack']) - iv.add_output('cp', val=1002.93, units='J/kg/K') + iv = self.add_subsystem("dv", IndepVarComp(), promotes_outputs=["cp", "*_1", "*_2", "*_3", "convergence_hack"]) + iv.add_output("cp", val=1002.93, units="J/kg/K") - iv.add_output('area_1', val=60, units='inch**2') - iv.add_output('delta_p_1', val=np.zeros((nn,)), units='Pa') - iv.add_output('heat_in_1', val=np.zeros((nn,)), units='W') - iv.add_output('pressure_recovery_1', val=np.ones((nn,))) - iv.add_output('loss_factor_1', val=0.0) + iv.add_output("area_1", val=60, units="inch**2") + iv.add_output("delta_p_1", val=np.zeros((nn,)), units="Pa") + iv.add_output("heat_in_1", val=np.zeros((nn,)), units="W") + iv.add_output("pressure_recovery_1", val=np.ones((nn,))) + iv.add_output("loss_factor_1", val=0.0) - iv.add_output('delta_p_2', val=np.ones((nn,))*0., units='Pa') - iv.add_output('heat_in_2', val=np.ones((nn,))*0., units='W') - iv.add_output('pressure_recovery_2', val=np.ones((nn,))) + iv.add_output("delta_p_2", val=np.ones((nn,)) * 0.0, units="Pa") + iv.add_output("heat_in_2", val=np.ones((nn,)) * 0.0, units="W") + iv.add_output("pressure_recovery_2", val=np.ones((nn,))) - iv.add_output('pressure_recovery_3', val=np.ones((nn,))) + iv.add_output("pressure_recovery_3", val=np.ones((nn,))) # iv.add_output('area_nozzle', val=58*np.ones((nn,)), units='inch**2') - iv.add_output('convergence_hack', val=-40, units='Pa') - dvlist = [['area_nozzle_in', 'area_nozzle', 58*np.ones((nn,)), 'inch**2']] - self.add_subsystem('dvpassthru',DVLabel(dvlist),promotes_inputs=["*"],promotes_outputs=["*"]) - - - self.add_subsystem('mdotguess',FlowMatcher(num_nodes=nn),promotes=['*']) - - self.add_subsystem('inlet', Inlet(num_nodes=nn), - promotes_inputs=[('p','p_inf'),('T','T_inf'),'Utrue']) - - self.add_subsystem('sta1', DuctStation(num_nodes=nn), promotes_inputs=['mdot','cp', - ('area','area_1'), - ('delta_p','delta_p_1'), - ('heat_in','heat_in_1'), - ('pressure_recovery','pressure_recovery_1')]) - self.connect('inlet.pt','sta1.pt_in') - self.connect('inlet.Tt','sta1.Tt_in') - self.connect('loss_factor_1','sta1.dynamic_pressure_loss_factor') - - - self.add_subsystem('sta2', DuctStation(num_nodes=nn), promotes_inputs=['mdot','cp', - ('area','area_2'), - ('delta_p','delta_p_2'), - ('heat_in','heat_in_2'), - ('pressure_recovery','pressure_recovery_2')]) - self.connect('sta1.pt_out','sta2.pt_in') - self.connect('sta1.Tt_out','sta2.Tt_in') + iv.add_output("convergence_hack", val=-40, units="Pa") + dvlist = [["area_nozzle_in", "area_nozzle", 58 * np.ones((nn,)), "inch**2"]] + self.add_subsystem("dvpassthru", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) + + self.add_subsystem("mdotguess", FlowMatcher(num_nodes=nn), promotes=["*"]) + + self.add_subsystem("inlet", Inlet(num_nodes=nn), promotes_inputs=[("p", "p_inf"), ("T", "T_inf"), "Utrue"]) + + self.add_subsystem( + "sta1", + DuctStation(num_nodes=nn), + promotes_inputs=[ + "mdot", + "cp", + ("area", "area_1"), + ("delta_p", "delta_p_1"), + ("heat_in", "heat_in_1"), + ("pressure_recovery", "pressure_recovery_1"), + ], + ) + self.connect("inlet.pt", "sta1.pt_in") + self.connect("inlet.Tt", "sta1.Tt_in") + self.connect("loss_factor_1", "sta1.dynamic_pressure_loss_factor") + + self.add_subsystem( + "sta2", + DuctStation(num_nodes=nn), + promotes_inputs=[ + "mdot", + "cp", + ("area", "area_2"), + ("delta_p", "delta_p_2"), + ("heat_in", "heat_in_2"), + ("pressure_recovery", "pressure_recovery_2"), + ], + ) + self.connect("sta1.pt_out", "sta2.pt_in") + self.connect("sta1.Tt_out", "sta2.Tt_in") # in to HXGroup: # duct.mdot -> mdot_cold @@ -1047,25 +1190,41 @@ def setup(self): # duct.sta2.T -> T_in_cold # duct.sta2.rho -> rho_cold - #out from HXGroup + # out from HXGroup # T_out_hot # delta_p_cold ->sta3.delta_p # heat_transfer -> sta3.heat_in # frontal_area -> 'area_2', 'area_3' - self.add_subsystem('sta3', DuctStation(num_nodes=nn), promotes_inputs=['mdot','cp', - ('pressure_recovery','pressure_recovery_3'), - ('area','area_3')]) - self.connect('sta2.pt_out','sta3.pt_in') - self.connect('sta2.Tt_out','sta3.Tt_in') - - self.add_subsystem('pexit',AddSubtractComp(output_name='p_exit',input_names=['p_inf','convergence_hack'],vec_size=[nn,1],units='Pa'),promotes_inputs=['*'],promotes_outputs=['*']) - self.add_subsystem('nozzle', OutletNozzle(num_nodes=nn), - promotes_inputs=['mdot','p_exit',('area','area_nozzle')], - promotes_outputs=['mdot_actual']) - self.connect('sta3.pt_out','nozzle.pt_in') - self.connect('sta3.Tt_out','nozzle.Tt') - - self.add_subsystem('force', NetForce(num_nodes=nn, cfg=self.options['cfg']), promotes_inputs=['mdot','p_inf',('Utrue_inf','Utrue'),'area_nozzle']) - self.connect('nozzle.p','force.p_nozzle') - self.connect('nozzle.rho','force.rho_nozzle') + self.add_subsystem( + "sta3", + DuctStation(num_nodes=nn), + promotes_inputs=["mdot", "cp", ("pressure_recovery", "pressure_recovery_3"), ("area", "area_3")], + ) + self.connect("sta2.pt_out", "sta3.pt_in") + self.connect("sta2.Tt_out", "sta3.Tt_in") + + self.add_subsystem( + "pexit", + AddSubtractComp( + output_name="p_exit", input_names=["p_inf", "convergence_hack"], vec_size=[nn, 1], units="Pa" + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "nozzle", + OutletNozzle(num_nodes=nn), + promotes_inputs=["mdot", "p_exit", ("area", "area_nozzle")], + promotes_outputs=["mdot_actual"], + ) + self.connect("sta3.pt_out", "nozzle.pt_in") + self.connect("sta3.Tt_out", "nozzle.Tt") + + self.add_subsystem( + "force", + NetForce(num_nodes=nn, cfg=self.options["cfg"]), + promotes_inputs=["mdot", "p_inf", ("Utrue_inf", "Utrue"), "area_nozzle"], + ) + self.connect("nozzle.p", "force.p_nozzle") + self.connect("nozzle.rho", "force.rho_nozzle") diff --git a/openconcept/thermal/heat_exchanger.py b/openconcept/thermal/heat_exchanger.py index 007d16a5..be59f07f 100644 --- a/openconcept/thermal/heat_exchanger.py +++ b/openconcept/thermal/heat_exchanger.py @@ -2,6 +2,7 @@ from openmdao.api import ExplicitComponent, IndepVarComp, Group from openconcept.utilities import DVLabel + class OffsetStripFinGeometry(ExplicitComponent): """ Computes geometric and solid parameters of a offset strip fin plate-fin heat exchanger. @@ -90,99 +91,103 @@ class OffsetStripFinGeometry(ExplicitComponent): # self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') def setup(self): - self.add_input('channel_width_hot', val=0.001, units='m') - self.add_input('channel_height_hot', val=0.001, units='m') - self.add_input('fin_length_hot', val=0.006, units='m') - self.add_input('channel_width_cold', val=0.001, units='m') - self.add_input('channel_height_cold', val=0.012, units='m') - self.add_input('fin_length_cold', val=0.006, units='m') - - self.add_input('fin_thickness', val=0.000102, units='m') - self.add_input('plate_thickness', val=0.0002, units='m') - self.add_input('case_thickness', val=0.002, units='m') - - self.add_input('n_wide_cold', val=100) - self.add_input('n_long_cold', val=3) - self.add_input('n_tall', val=20) - self.add_input('material_rho', val=2700.0, units='kg/m**3') - - self.add_output('component_weight', units='kg') - self.add_output('length_overall', units='m') - self.add_output('height_overall', units='m') - self.add_output('width_overall', units='m') - self.add_output('frontal_area', units='m**2') - - self.add_output('xs_area_cold', units='m**2') - self.add_output('heat_transfer_area_cold', units='m**2') - self.add_output('dh_cold', units='m') - self.add_output('fin_area_ratio_cold') - self.add_output('contraction_ratio_cold') - self.add_output('alpha_cold') - self.add_output('delta_cold') - self.add_output('gamma_cold') - - self.add_output('xs_area_hot', units='m**2') - self.add_output('heat_transfer_area_hot', units='m**2') - self.add_output('dh_hot', units='m') - self.add_output('fin_area_ratio_hot') - self.add_output('contraction_ratio_hot') - self.add_output('alpha_hot') - self.add_output('delta_hot') - self.add_output('gamma_hot') - - self.declare_partials(['*'], ['*'], method='cs') + self.add_input("channel_width_hot", val=0.001, units="m") + self.add_input("channel_height_hot", val=0.001, units="m") + self.add_input("fin_length_hot", val=0.006, units="m") + self.add_input("channel_width_cold", val=0.001, units="m") + self.add_input("channel_height_cold", val=0.012, units="m") + self.add_input("fin_length_cold", val=0.006, units="m") + + self.add_input("fin_thickness", val=0.000102, units="m") + self.add_input("plate_thickness", val=0.0002, units="m") + self.add_input("case_thickness", val=0.002, units="m") + + self.add_input("n_wide_cold", val=100) + self.add_input("n_long_cold", val=3) + self.add_input("n_tall", val=20) + self.add_input("material_rho", val=2700.0, units="kg/m**3") + + self.add_output("component_weight", units="kg") + self.add_output("length_overall", units="m") + self.add_output("height_overall", units="m") + self.add_output("width_overall", units="m") + self.add_output("frontal_area", units="m**2") + + self.add_output("xs_area_cold", units="m**2") + self.add_output("heat_transfer_area_cold", units="m**2") + self.add_output("dh_cold", units="m") + self.add_output("fin_area_ratio_cold") + self.add_output("contraction_ratio_cold") + self.add_output("alpha_cold") + self.add_output("delta_cold") + self.add_output("gamma_cold") + + self.add_output("xs_area_hot", units="m**2") + self.add_output("heat_transfer_area_hot", units="m**2") + self.add_output("dh_hot", units="m") + self.add_output("fin_area_ratio_hot") + self.add_output("contraction_ratio_hot") + self.add_output("alpha_hot") + self.add_output("delta_hot") + self.add_output("gamma_hot") + + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - t_f = inputs['fin_thickness'] - t_p = inputs['plate_thickness'] - t_c = inputs['case_thickness'] - n_tall = inputs['n_tall'] + t_f = inputs["fin_thickness"] + t_p = inputs["plate_thickness"] + t_c = inputs["case_thickness"] + n_tall = inputs["n_tall"] - w_c = inputs['channel_width_cold'] - h_c = inputs['channel_height_cold'] - l_c = inputs['fin_length_cold'] - n_wide_c = inputs['n_wide_cold'] - n_long_c = inputs['n_long_cold'] + w_c = inputs["channel_width_cold"] + h_c = inputs["channel_height_cold"] + l_c = inputs["fin_length_cold"] + n_wide_c = inputs["n_wide_cold"] + n_long_c = inputs["n_long_cold"] - w_h = inputs['channel_width_hot'] - h_h = inputs['channel_height_hot'] - l_h = inputs['fin_length_hot'] + w_h = inputs["channel_width_hot"] + h_h = inputs["channel_height_hot"] + l_h = inputs["fin_length_hot"] # compute overall properties - outputs['height_overall'] = (2 * (t_f + t_p) + h_c + h_h) * n_tall - outputs['width_overall'] = (t_f + w_c) * n_wide_c - outputs['length_overall'] = l_c * n_long_c - outputs['frontal_area'] = outputs['width_overall'] * outputs['height_overall'] + outputs["height_overall"] = (2 * (t_f + t_p) + h_c + h_h) * n_tall + outputs["width_overall"] = (t_f + w_c) * n_wide_c + outputs["length_overall"] = l_c * n_long_c + outputs["frontal_area"] = outputs["width_overall"] * outputs["height_overall"] # compute cold side geometric properties # outputs['dh_cold'] = 2 * w_c * h_c / (w_c + h_c) # special formula for dh (maybe accounts for bend radii?) from Manglik and Bergles paper - outputs['dh_cold'] = 4 * w_c * h_c * l_c / (2 * (w_c * l_c + h_c * l_c + t_f * h_c) + t_f * w_c) - outputs['xs_area_cold'] = w_c * h_c * n_wide_c * n_tall - outputs['heat_transfer_area_cold'] = 2 * (w_c + h_c) * l_c * n_long_c * n_wide_c * n_tall - outputs['fin_area_ratio_cold'] = h_c / (h_c + w_c) - outputs['contraction_ratio_cold'] = outputs['xs_area_cold'] / outputs['frontal_area'] - outputs['alpha_cold'] = w_c / h_c - outputs['delta_cold'] = t_f / l_c - outputs['gamma_cold'] = t_f / w_c - - n_wide_h = outputs['length_overall'] / (w_h + t_f) - n_long_h = outputs['width_overall'] / l_h - - outputs['dh_hot'] = 2 * w_h * h_h / (w_h + h_h) - outputs['xs_area_hot'] = w_h * h_h * n_wide_h * n_tall - outputs['heat_transfer_area_hot'] = 2 * (w_h + h_h) * l_h * n_long_h * n_wide_h * n_tall - outputs['fin_area_ratio_hot'] = h_h / (h_h + w_h) - outputs['contraction_ratio_hot'] = outputs['xs_area_hot'] / outputs['height_overall'] / outputs['length_overall'] - outputs['alpha_hot'] = w_h / h_h - outputs['delta_hot'] = t_f / l_h - outputs['gamma_hot'] = t_f / w_h - - plate_volume = outputs['length_overall'] * outputs['width_overall'] * 2 * t_p * n_tall - fin_volume = ((w_c + h_c + t_f) * l_c * n_long_c * t_f * n_wide_c * n_tall + - (w_h + h_h + t_f) * l_h * n_long_h * t_f * n_wide_h * n_tall) - case_volume = t_c * outputs['length_overall'] * 2 * (outputs['height_overall'] + outputs['width_overall']) - outputs['component_weight'] = (plate_volume + fin_volume + case_volume) * inputs['material_rho'] + outputs["dh_cold"] = 4 * w_c * h_c * l_c / (2 * (w_c * l_c + h_c * l_c + t_f * h_c) + t_f * w_c) + outputs["xs_area_cold"] = w_c * h_c * n_wide_c * n_tall + outputs["heat_transfer_area_cold"] = 2 * (w_c + h_c) * l_c * n_long_c * n_wide_c * n_tall + outputs["fin_area_ratio_cold"] = h_c / (h_c + w_c) + outputs["contraction_ratio_cold"] = outputs["xs_area_cold"] / outputs["frontal_area"] + outputs["alpha_cold"] = w_c / h_c + outputs["delta_cold"] = t_f / l_c + outputs["gamma_cold"] = t_f / w_c + + n_wide_h = outputs["length_overall"] / (w_h + t_f) + n_long_h = outputs["width_overall"] / l_h + + outputs["dh_hot"] = 2 * w_h * h_h / (w_h + h_h) + outputs["xs_area_hot"] = w_h * h_h * n_wide_h * n_tall + outputs["heat_transfer_area_hot"] = 2 * (w_h + h_h) * l_h * n_long_h * n_wide_h * n_tall + outputs["fin_area_ratio_hot"] = h_h / (h_h + w_h) + outputs["contraction_ratio_hot"] = ( + outputs["xs_area_hot"] / outputs["height_overall"] / outputs["length_overall"] + ) + outputs["alpha_hot"] = w_h / h_h + outputs["delta_hot"] = t_f / l_h + outputs["gamma_hot"] = t_f / w_h + + plate_volume = outputs["length_overall"] * outputs["width_overall"] * 2 * t_p * n_tall + fin_volume = (w_c + h_c + t_f) * l_c * n_long_c * t_f * n_wide_c * n_tall + ( + w_h + h_h + t_f + ) * l_h * n_long_h * t_f * n_wide_h * n_tall + case_volume = t_c * outputs["length_overall"] * 2 * (outputs["height_overall"] + outputs["width_overall"]) + outputs["component_weight"] = (plate_volume + fin_volume + case_volume) * inputs["material_rho"] + class OffsetStripFinData(ExplicitComponent): """ @@ -230,83 +235,138 @@ class OffsetStripFinData(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('Re_dh_cold', val=np.ones((nn,)), shape=(nn,)) - self.add_input('alpha_cold') - self.add_input('delta_cold') - self.add_input('gamma_cold') - self.add_input('Re_dh_hot', val=np.ones((nn,)), shape=(nn,)) - self.add_input('alpha_hot') - self.add_input('delta_hot') - self.add_input('gamma_hot') - self.add_output('j_cold', shape=(nn,)) - self.add_output('f_cold', shape=(nn,)) - self.add_output('j_hot', shape=(nn,)) - self.add_output('f_hot', shape=(nn,)) + nn = self.options["num_nodes"] + self.add_input("Re_dh_cold", val=np.ones((nn,)), shape=(nn,)) + self.add_input("alpha_cold") + self.add_input("delta_cold") + self.add_input("gamma_cold") + self.add_input("Re_dh_hot", val=np.ones((nn,)), shape=(nn,)) + self.add_input("alpha_hot") + self.add_input("delta_hot") + self.add_input("gamma_hot") + self.add_output("j_cold", shape=(nn,)) + self.add_output("f_cold", shape=(nn,)) + self.add_output("j_hot", shape=(nn,)) + self.add_output("f_hot", shape=(nn,)) arange = np.arange(0, nn) - self.declare_partials(['j_cold', 'f_cold'], ['alpha_cold','delta_cold','gamma_cold'], method='cs') - self.declare_partials(['j_hot', 'f_hot'], ['alpha_hot','delta_hot','gamma_hot'], method='cs') - self.declare_partials(['j_cold', 'f_cold'], ['Re_dh_cold'], rows=arange, cols=arange) - self.declare_partials(['j_hot', 'f_hot'], ['Re_dh_hot'], rows=arange, cols=arange) + self.declare_partials(["j_cold", "f_cold"], ["alpha_cold", "delta_cold", "gamma_cold"], method="cs") + self.declare_partials(["j_hot", "f_hot"], ["alpha_hot", "delta_hot", "gamma_hot"], method="cs") + self.declare_partials(["j_cold", "f_cold"], ["Re_dh_cold"], rows=arange, cols=arange) + self.declare_partials(["j_hot", "f_hot"], ["Re_dh_hot"], rows=arange, cols=arange) def compute(self, inputs, outputs): - jc_1 = inputs['alpha_cold']**-0.1541 * inputs['delta_cold']**0.1499 * inputs['gamma_cold']**-0.0678 - jc_2 = inputs['alpha_cold']**0.504 * inputs['delta_cold']**0.456 * inputs['gamma_cold']**-1.055 - fc_1 = inputs['alpha_cold']**-0.1856 * inputs['delta_cold']**0.3053 * inputs['gamma_cold']**-0.2659 - fc_2 = inputs['alpha_cold']**0.92 * inputs['delta_cold']**3.767 * inputs['gamma_cold']**0.236 - if np.min(inputs['Re_dh_cold']) <= 0.0: - raise ValueError(self.msginfo, inputs['Re_dh_cold']) - outputs['j_cold'] = (0.6522*inputs['Re_dh_cold']**-0.5403 * jc_1 * - (1 + 5.269e-5*inputs['Re_dh_cold']**1.34 * jc_2)**0.1) - outputs['f_cold'] = (9.6243*inputs['Re_dh_cold']**-0.7422 * fc_1 * - (1 + 7.669e-8*inputs['Re_dh_cold']**4.429 * fc_2)**0.1) - - jh_1 = inputs['alpha_hot']**-0.1541 * inputs['delta_hot']**0.1499 * inputs['gamma_hot']**-0.0678 - jh_2 = inputs['alpha_hot']**0.504 * inputs['delta_hot']**0.456 * inputs['gamma_hot']**-1.055 - fh_1 = inputs['alpha_hot']**-0.1856 * inputs['delta_hot']**0.3053 * inputs['gamma_hot']**-0.2659 - fh_2 = inputs['alpha_hot']**0.92 * inputs['delta_hot']**3.767 * inputs['gamma_hot']**0.236 - - outputs['j_hot'] = (0.6522*inputs['Re_dh_hot']**-0.5403 * jh_1 * - (1 + 5.269e-5*inputs['Re_dh_hot']**1.34 * jh_2)**0.1) - outputs['f_hot'] = (9.6243*inputs['Re_dh_hot']**-0.7422 * fh_1 * - (1 + 7.669e-8*inputs['Re_dh_hot']**4.429 * fh_2)**0.1) + jc_1 = inputs["alpha_cold"] ** -0.1541 * inputs["delta_cold"] ** 0.1499 * inputs["gamma_cold"] ** -0.0678 + jc_2 = inputs["alpha_cold"] ** 0.504 * inputs["delta_cold"] ** 0.456 * inputs["gamma_cold"] ** -1.055 + fc_1 = inputs["alpha_cold"] ** -0.1856 * inputs["delta_cold"] ** 0.3053 * inputs["gamma_cold"] ** -0.2659 + fc_2 = inputs["alpha_cold"] ** 0.92 * inputs["delta_cold"] ** 3.767 * inputs["gamma_cold"] ** 0.236 + if np.min(inputs["Re_dh_cold"]) <= 0.0: + raise ValueError(self.msginfo, inputs["Re_dh_cold"]) + outputs["j_cold"] = ( + 0.6522 + * inputs["Re_dh_cold"] ** -0.5403 + * jc_1 + * (1 + 5.269e-5 * inputs["Re_dh_cold"] ** 1.34 * jc_2) ** 0.1 + ) + outputs["f_cold"] = ( + 9.6243 + * inputs["Re_dh_cold"] ** -0.7422 + * fc_1 + * (1 + 7.669e-8 * inputs["Re_dh_cold"] ** 4.429 * fc_2) ** 0.1 + ) + + jh_1 = inputs["alpha_hot"] ** -0.1541 * inputs["delta_hot"] ** 0.1499 * inputs["gamma_hot"] ** -0.0678 + jh_2 = inputs["alpha_hot"] ** 0.504 * inputs["delta_hot"] ** 0.456 * inputs["gamma_hot"] ** -1.055 + fh_1 = inputs["alpha_hot"] ** -0.1856 * inputs["delta_hot"] ** 0.3053 * inputs["gamma_hot"] ** -0.2659 + fh_2 = inputs["alpha_hot"] ** 0.92 * inputs["delta_hot"] ** 3.767 * inputs["gamma_hot"] ** 0.236 + + outputs["j_hot"] = ( + 0.6522 * inputs["Re_dh_hot"] ** -0.5403 * jh_1 * (1 + 5.269e-5 * inputs["Re_dh_hot"] ** 1.34 * jh_2) ** 0.1 + ) + outputs["f_hot"] = ( + 9.6243 * inputs["Re_dh_hot"] ** -0.7422 * fh_1 * (1 + 7.669e-8 * inputs["Re_dh_hot"] ** 4.429 * fh_2) ** 0.1 + ) def compute_partials(self, inputs, J): - jc_1 = inputs['alpha_cold']**-0.1541 * inputs['delta_cold']**0.1499 * inputs['gamma_cold']**-0.0678 - jc_2 = inputs['alpha_cold']**0.504 * inputs['delta_cold']**0.456 * inputs['gamma_cold']**-1.055 - fc_1 = inputs['alpha_cold']**-0.1856 * inputs['delta_cold']**0.3053 * inputs['gamma_cold']**-0.2659 - fc_2 = inputs['alpha_cold']**0.92 * inputs['delta_cold']**3.767 * inputs['gamma_cold']**0.236 - - J['j_cold', 'Re_dh_cold'] = ((0.6522*-0.5403*inputs['Re_dh_cold']**-1.5403 * jc_1 * - (1 + 5.269e-5*inputs['Re_dh_cold']**1.34 * jc_2)**0.1) + - (0.6522*inputs['Re_dh_cold']**-0.5403 * jc_1 * - 0.1 * (1 + 5.269e-5*inputs['Re_dh_cold']**1.34 * jc_2)**-0.9 * - 5.269e-5*1.34*inputs['Re_dh_cold']**0.34 * jc_2)) - J['f_cold', 'Re_dh_cold'] = ((9.6243*-0.7422*inputs['Re_dh_cold']**-1.7422 * fc_1 * - (1 + 7.669e-8*inputs['Re_dh_cold']**4.429 * fc_2)**0.1) + - (9.6243*inputs['Re_dh_cold']**-0.7422 * fc_1 * - 0.1 * (1 + 7.669e-8*inputs['Re_dh_cold']**4.429 * fc_2)**-0.9) * - 7.669e-8*4.429*inputs['Re_dh_cold']**3.429 * fc_2) - - jh_1 = inputs['alpha_hot']**-0.1541 * inputs['delta_hot']**0.1499 * inputs['gamma_hot']**-0.0678 - jh_2 = inputs['alpha_hot']**0.504 * inputs['delta_hot']**0.456 * inputs['gamma_hot']**-1.055 - fh_1 = inputs['alpha_hot']**-0.1856 * inputs['delta_hot']**0.3053 * inputs['gamma_hot']**-0.2659 - fh_2 = inputs['alpha_hot']**0.92 * inputs['delta_hot']**3.767 * inputs['gamma_hot']**0.236 - - J['j_hot', 'Re_dh_hot'] = ((0.6522*-0.5403*inputs['Re_dh_hot']**-1.5403 * jh_1 * - (1 + 5.269e-5*inputs['Re_dh_hot']**1.34 * jh_2)**0.1) + - (0.6522*inputs['Re_dh_hot']**-0.5403 * jh_1 * - 0.1 * (1 + 5.269e-5*inputs['Re_dh_hot']**1.34 * jh_2)**-0.9 * - 5.269e-5*1.34*inputs['Re_dh_hot']**0.34 * jh_2)) - J['f_hot', 'Re_dh_hot'] = ((9.6243*-0.7422*inputs['Re_dh_hot']**-1.7422 * fh_1 * - (1 + 7.669e-8*inputs['Re_dh_hot']**4.429 * fh_2)**0.1) + - (9.6243*inputs['Re_dh_hot']**-0.7422 * fh_1 * - 0.1 * (1 + 7.669e-8*inputs['Re_dh_hot']**4.429 * fh_2)**-0.9) * - 7.669e-8*4.429*inputs['Re_dh_hot']**3.429 * fh_2) + jc_1 = inputs["alpha_cold"] ** -0.1541 * inputs["delta_cold"] ** 0.1499 * inputs["gamma_cold"] ** -0.0678 + jc_2 = inputs["alpha_cold"] ** 0.504 * inputs["delta_cold"] ** 0.456 * inputs["gamma_cold"] ** -1.055 + fc_1 = inputs["alpha_cold"] ** -0.1856 * inputs["delta_cold"] ** 0.3053 * inputs["gamma_cold"] ** -0.2659 + fc_2 = inputs["alpha_cold"] ** 0.92 * inputs["delta_cold"] ** 3.767 * inputs["gamma_cold"] ** 0.236 + + J["j_cold", "Re_dh_cold"] = ( + 0.6522 + * -0.5403 + * inputs["Re_dh_cold"] ** -1.5403 + * jc_1 + * (1 + 5.269e-5 * inputs["Re_dh_cold"] ** 1.34 * jc_2) ** 0.1 + ) + ( + 0.6522 + * inputs["Re_dh_cold"] ** -0.5403 + * jc_1 + * 0.1 + * (1 + 5.269e-5 * inputs["Re_dh_cold"] ** 1.34 * jc_2) ** -0.9 + * 5.269e-5 + * 1.34 + * inputs["Re_dh_cold"] ** 0.34 + * jc_2 + ) + J["f_cold", "Re_dh_cold"] = ( + 9.6243 + * -0.7422 + * inputs["Re_dh_cold"] ** -1.7422 + * fc_1 + * (1 + 7.669e-8 * inputs["Re_dh_cold"] ** 4.429 * fc_2) ** 0.1 + ) + ( + 9.6243 + * inputs["Re_dh_cold"] ** -0.7422 + * fc_1 + * 0.1 + * (1 + 7.669e-8 * inputs["Re_dh_cold"] ** 4.429 * fc_2) ** -0.9 + ) * 7.669e-8 * 4.429 * inputs[ + "Re_dh_cold" + ] ** 3.429 * fc_2 + + jh_1 = inputs["alpha_hot"] ** -0.1541 * inputs["delta_hot"] ** 0.1499 * inputs["gamma_hot"] ** -0.0678 + jh_2 = inputs["alpha_hot"] ** 0.504 * inputs["delta_hot"] ** 0.456 * inputs["gamma_hot"] ** -1.055 + fh_1 = inputs["alpha_hot"] ** -0.1856 * inputs["delta_hot"] ** 0.3053 * inputs["gamma_hot"] ** -0.2659 + fh_2 = inputs["alpha_hot"] ** 0.92 * inputs["delta_hot"] ** 3.767 * inputs["gamma_hot"] ** 0.236 + + J["j_hot", "Re_dh_hot"] = ( + 0.6522 + * -0.5403 + * inputs["Re_dh_hot"] ** -1.5403 + * jh_1 + * (1 + 5.269e-5 * inputs["Re_dh_hot"] ** 1.34 * jh_2) ** 0.1 + ) + ( + 0.6522 + * inputs["Re_dh_hot"] ** -0.5403 + * jh_1 + * 0.1 + * (1 + 5.269e-5 * inputs["Re_dh_hot"] ** 1.34 * jh_2) ** -0.9 + * 5.269e-5 + * 1.34 + * inputs["Re_dh_hot"] ** 0.34 + * jh_2 + ) + J["f_hot", "Re_dh_hot"] = ( + 9.6243 + * -0.7422 + * inputs["Re_dh_hot"] ** -1.7422 + * fh_1 + * (1 + 7.669e-8 * inputs["Re_dh_hot"] ** 4.429 * fh_2) ** 0.1 + ) + ( + 9.6243 + * inputs["Re_dh_hot"] ** -0.7422 + * fh_1 + * 0.1 + * (1 + 7.669e-8 * inputs["Re_dh_hot"] ** 4.429 * fh_2) ** -0.9 + ) * 7.669e-8 * 4.429 * inputs[ + "Re_dh_hot" + ] ** 3.429 * fh_2 + class HydraulicDiameterReynoldsNumber(ExplicitComponent): """ @@ -344,42 +404,56 @@ class HydraulicDiameterReynoldsNumber(ExplicitComponent): num_nodes : int Number of analysis points to run (scalar, dimensionless) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('mdot_cold', val=np.ones((nn,)), shape=(nn,), units='kg/s') - self.add_input('mu_cold', units='kg/m/s') - self.add_input('xs_area_cold', units='m**2') - self.add_input('dh_cold', units='m') - self.add_input('mdot_hot', val=np.ones((nn,)), shape=(nn,), units='kg/s') - self.add_input('mu_hot', units='kg/m/s') - self.add_input('xs_area_hot', units='m**2') - self.add_input('dh_hot', units='m') - - self.add_output('Re_dh_cold', shape=(nn,), lower=0.01) - self.add_output('Re_dh_hot', shape=(nn,)) + nn = self.options["num_nodes"] + self.add_input("mdot_cold", val=np.ones((nn,)), shape=(nn,), units="kg/s") + self.add_input("mu_cold", units="kg/m/s") + self.add_input("xs_area_cold", units="m**2") + self.add_input("dh_cold", units="m") + self.add_input("mdot_hot", val=np.ones((nn,)), shape=(nn,), units="kg/s") + self.add_input("mu_hot", units="kg/m/s") + self.add_input("xs_area_hot", units="m**2") + self.add_input("dh_hot", units="m") + + self.add_output("Re_dh_cold", shape=(nn,), lower=0.01) + self.add_output("Re_dh_hot", shape=(nn,)) arange = np.arange(0, nn) - self.declare_partials(['Re_dh_cold'], ['mdot_cold'], rows=arange, cols=arange) - self.declare_partials(['Re_dh_cold'], ['mu_cold', 'xs_area_cold', 'dh_cold'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) - self.declare_partials(['Re_dh_hot'], ['mdot_hot'], rows=arange, cols=arange) - self.declare_partials(['Re_dh_hot'], ['mu_hot', 'xs_area_hot', 'dh_hot'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["Re_dh_cold"], ["mdot_cold"], rows=arange, cols=arange) + self.declare_partials( + ["Re_dh_cold"], ["mu_cold", "xs_area_cold", "dh_cold"], rows=arange, cols=np.zeros((nn,), dtype=np.int32) + ) + self.declare_partials(["Re_dh_hot"], ["mdot_hot"], rows=arange, cols=arange) + self.declare_partials( + ["Re_dh_hot"], ["mu_hot", "xs_area_hot", "dh_hot"], rows=arange, cols=np.zeros((nn,), dtype=np.int32) + ) def compute(self, inputs, outputs): - outputs['Re_dh_cold'] = inputs['mdot_cold'] * inputs['dh_cold'] / inputs['xs_area_cold'] / inputs['mu_cold'] - outputs['Re_dh_hot'] = inputs['mdot_hot'] * inputs['dh_hot'] / inputs['xs_area_hot'] / inputs['mu_hot'] + outputs["Re_dh_cold"] = inputs["mdot_cold"] * inputs["dh_cold"] / inputs["xs_area_cold"] / inputs["mu_cold"] + outputs["Re_dh_hot"] = inputs["mdot_hot"] * inputs["dh_hot"] / inputs["xs_area_hot"] / inputs["mu_hot"] def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - J['Re_dh_cold', 'mdot_cold'] = inputs['dh_cold'] / inputs['xs_area_cold'] / inputs['mu_cold'] * np.ones((nn,)) - J['Re_dh_cold', 'dh_cold'] = inputs['mdot_cold'] / inputs['xs_area_cold'] / inputs['mu_cold'] - J['Re_dh_cold', 'xs_area_cold'] = - inputs['mdot_cold'] * inputs['dh_cold'] / inputs['xs_area_cold'] ** 2 / inputs['mu_cold'] - J['Re_dh_cold', 'mu_cold'] = - inputs['mdot_cold'] * inputs['dh_cold'] / inputs['xs_area_cold'] / inputs['mu_cold'] ** 2 - J['Re_dh_hot', 'mdot_hot'] = inputs['dh_hot'] / inputs['xs_area_hot'] / inputs['mu_hot'] * np.ones((nn,)) - J['Re_dh_hot', 'dh_hot'] = inputs['mdot_hot'] / inputs['xs_area_hot'] / inputs['mu_hot'] - J['Re_dh_hot', 'xs_area_hot'] = - inputs['mdot_hot'] * inputs['dh_hot'] / inputs['xs_area_hot'] ** 2 / inputs['mu_hot'] - J['Re_dh_hot', 'mu_hot'] = - inputs['mdot_hot'] * inputs['dh_hot'] / inputs['xs_area_hot'] / inputs['mu_hot'] ** 2 + nn = self.options["num_nodes"] + J["Re_dh_cold", "mdot_cold"] = inputs["dh_cold"] / inputs["xs_area_cold"] / inputs["mu_cold"] * np.ones((nn,)) + J["Re_dh_cold", "dh_cold"] = inputs["mdot_cold"] / inputs["xs_area_cold"] / inputs["mu_cold"] + J["Re_dh_cold", "xs_area_cold"] = ( + -inputs["mdot_cold"] * inputs["dh_cold"] / inputs["xs_area_cold"] ** 2 / inputs["mu_cold"] + ) + J["Re_dh_cold", "mu_cold"] = ( + -inputs["mdot_cold"] * inputs["dh_cold"] / inputs["xs_area_cold"] / inputs["mu_cold"] ** 2 + ) + J["Re_dh_hot", "mdot_hot"] = inputs["dh_hot"] / inputs["xs_area_hot"] / inputs["mu_hot"] * np.ones((nn,)) + J["Re_dh_hot", "dh_hot"] = inputs["mdot_hot"] / inputs["xs_area_hot"] / inputs["mu_hot"] + J["Re_dh_hot", "xs_area_hot"] = ( + -inputs["mdot_hot"] * inputs["dh_hot"] / inputs["xs_area_hot"] ** 2 / inputs["mu_hot"] + ) + J["Re_dh_hot", "mu_hot"] = ( + -inputs["mdot_hot"] * inputs["dh_hot"] / inputs["xs_area_hot"] / inputs["mu_hot"] ** 2 + ) + class NusseltFromColburnJ(ExplicitComponent): """ @@ -420,58 +494,135 @@ class NusseltFromColburnJ(ExplicitComponent): num_nodes : int Number of analysis points to run (scalar, dimensionless) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('Re_dh_cold', shape=(nn,)) - self.add_input('j_cold', shape=(nn,)) - self.add_input('k_cold', units='W/m/K') - self.add_input('mu_cold', units='kg/m/s') - self.add_input('cp_cold', units='J/kg/K') - self.add_input('Re_dh_hot', shape=(nn,)) - self.add_input('j_hot', shape=(nn,)) - self.add_input('k_hot', units='W/m/K') - self.add_input('mu_hot', units='kg/m/s') - self.add_input('cp_hot', units='J/kg/K') - - self.add_output('Nu_dh_cold', shape=(nn,), lower=0.001) - self.add_output('Nu_dh_hot', shape=(nn,)) + nn = self.options["num_nodes"] + self.add_input("Re_dh_cold", shape=(nn,)) + self.add_input("j_cold", shape=(nn,)) + self.add_input("k_cold", units="W/m/K") + self.add_input("mu_cold", units="kg/m/s") + self.add_input("cp_cold", units="J/kg/K") + self.add_input("Re_dh_hot", shape=(nn,)) + self.add_input("j_hot", shape=(nn,)) + self.add_input("k_hot", units="W/m/K") + self.add_input("mu_hot", units="kg/m/s") + self.add_input("cp_hot", units="J/kg/K") + + self.add_output("Nu_dh_cold", shape=(nn,), lower=0.001) + self.add_output("Nu_dh_hot", shape=(nn,)) arange = np.arange(0, nn) - self.declare_partials(['Nu_dh_cold'], ['j_cold', 'Re_dh_cold'], rows=arange, cols=arange) - self.declare_partials(['Nu_dh_cold'], ['k_cold', 'mu_cold', 'cp_cold'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) - self.declare_partials(['Nu_dh_hot'], ['j_hot', 'Re_dh_hot'], rows=arange, cols=arange) - self.declare_partials(['Nu_dh_hot'], ['k_hot', 'mu_hot', 'cp_hot'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["Nu_dh_cold"], ["j_cold", "Re_dh_cold"], rows=arange, cols=arange) + self.declare_partials( + ["Nu_dh_cold"], ["k_cold", "mu_cold", "cp_cold"], rows=arange, cols=np.zeros((nn,), dtype=np.int32) + ) + self.declare_partials(["Nu_dh_hot"], ["j_hot", "Re_dh_hot"], rows=arange, cols=arange) + self.declare_partials( + ["Nu_dh_hot"], ["k_hot", "mu_hot", "cp_hot"], rows=arange, cols=np.zeros((nn,), dtype=np.int32) + ) def compute(self, inputs, outputs): - outputs['Nu_dh_cold'] = (inputs['j_cold'] * inputs['Re_dh_cold'] * - inputs['mu_cold'] ** (1/3) * inputs['cp_cold'] ** (1/3) / inputs['k_cold'] ** (1/3)) - outputs['Nu_dh_hot'] = (inputs['j_hot'] * inputs['Re_dh_hot'] * - inputs['mu_hot'] ** (1/3) * inputs['cp_hot'] ** (1/3) / inputs['k_hot'] ** (1/3)) + outputs["Nu_dh_cold"] = ( + inputs["j_cold"] + * inputs["Re_dh_cold"] + * inputs["mu_cold"] ** (1 / 3) + * inputs["cp_cold"] ** (1 / 3) + / inputs["k_cold"] ** (1 / 3) + ) + outputs["Nu_dh_hot"] = ( + inputs["j_hot"] + * inputs["Re_dh_hot"] + * inputs["mu_hot"] ** (1 / 3) + * inputs["cp_hot"] ** (1 / 3) + / inputs["k_hot"] ** (1 / 3) + ) def compute_partials(self, inputs, J): - J['Nu_dh_cold', 'mu_cold'] = 1 / 3 * (inputs['j_cold'] * inputs['Re_dh_cold'] * - inputs['mu_cold'] ** (-2/3) * inputs['cp_cold'] ** (1/3) / inputs['k_cold'] ** (1/3)) - J['Nu_dh_cold', 'cp_cold'] = 1 / 3 * (inputs['j_cold'] * inputs['Re_dh_cold'] * - inputs['mu_cold'] ** (1/3) * inputs['cp_cold'] ** (-2/3) / inputs['k_cold'] ** (1/3)) - J['Nu_dh_cold', 'k_cold'] = -1 / 3 * (inputs['j_cold'] * inputs['Re_dh_cold'] * - inputs['mu_cold'] ** (1/3) * inputs['cp_cold'] ** (1/3) / inputs['k_cold'] ** (4/3)) - J['Nu_dh_cold', 'j_cold'] = (inputs['Re_dh_cold'] * - inputs['mu_cold'] ** (1/3) * inputs['cp_cold'] ** (1/3) / inputs['k_cold'] ** (1/3)) - J['Nu_dh_cold', 'Re_dh_cold'] = (inputs['j_cold'] * - inputs['mu_cold'] ** (1/3) * inputs['cp_cold'] ** (1/3) / inputs['k_cold'] ** (1/3)) - - J['Nu_dh_hot', 'mu_hot'] = 1 / 3 * (inputs['j_hot'] * inputs['Re_dh_hot'] * - inputs['mu_hot'] ** (-2/3) * inputs['cp_hot'] ** (1/3) / inputs['k_hot'] ** (1/3)) - J['Nu_dh_hot', 'cp_hot'] = 1 / 3 * (inputs['j_hot'] * inputs['Re_dh_hot'] * - inputs['mu_hot'] ** (1/3) * inputs['cp_hot'] ** (-2/3) / inputs['k_hot'] ** (1/3)) - J['Nu_dh_hot', 'k_hot'] = -1 / 3 * (inputs['j_hot'] * inputs['Re_dh_hot'] * - inputs['mu_hot'] ** (1/3) * inputs['cp_hot'] ** (1/3) / inputs['k_hot'] ** (4/3)) - J['Nu_dh_hot', 'j_hot'] = (inputs['Re_dh_hot'] * - inputs['mu_hot'] ** (1/3) * inputs['cp_hot'] ** (1/3) / inputs['k_hot'] ** (1/3)) - J['Nu_dh_hot', 'Re_dh_hot'] = (inputs['j_hot'] * - inputs['mu_hot'] ** (1/3) * inputs['cp_hot'] ** (1/3) / inputs['k_hot'] ** (1/3)) + J["Nu_dh_cold", "mu_cold"] = ( + 1 + / 3 + * ( + inputs["j_cold"] + * inputs["Re_dh_cold"] + * inputs["mu_cold"] ** (-2 / 3) + * inputs["cp_cold"] ** (1 / 3) + / inputs["k_cold"] ** (1 / 3) + ) + ) + J["Nu_dh_cold", "cp_cold"] = ( + 1 + / 3 + * ( + inputs["j_cold"] + * inputs["Re_dh_cold"] + * inputs["mu_cold"] ** (1 / 3) + * inputs["cp_cold"] ** (-2 / 3) + / inputs["k_cold"] ** (1 / 3) + ) + ) + J["Nu_dh_cold", "k_cold"] = ( + -1 + / 3 + * ( + inputs["j_cold"] + * inputs["Re_dh_cold"] + * inputs["mu_cold"] ** (1 / 3) + * inputs["cp_cold"] ** (1 / 3) + / inputs["k_cold"] ** (4 / 3) + ) + ) + J["Nu_dh_cold", "j_cold"] = ( + inputs["Re_dh_cold"] + * inputs["mu_cold"] ** (1 / 3) + * inputs["cp_cold"] ** (1 / 3) + / inputs["k_cold"] ** (1 / 3) + ) + J["Nu_dh_cold", "Re_dh_cold"] = ( + inputs["j_cold"] * inputs["mu_cold"] ** (1 / 3) * inputs["cp_cold"] ** (1 / 3) / inputs["k_cold"] ** (1 / 3) + ) + + J["Nu_dh_hot", "mu_hot"] = ( + 1 + / 3 + * ( + inputs["j_hot"] + * inputs["Re_dh_hot"] + * inputs["mu_hot"] ** (-2 / 3) + * inputs["cp_hot"] ** (1 / 3) + / inputs["k_hot"] ** (1 / 3) + ) + ) + J["Nu_dh_hot", "cp_hot"] = ( + 1 + / 3 + * ( + inputs["j_hot"] + * inputs["Re_dh_hot"] + * inputs["mu_hot"] ** (1 / 3) + * inputs["cp_hot"] ** (-2 / 3) + / inputs["k_hot"] ** (1 / 3) + ) + ) + J["Nu_dh_hot", "k_hot"] = ( + -1 + / 3 + * ( + inputs["j_hot"] + * inputs["Re_dh_hot"] + * inputs["mu_hot"] ** (1 / 3) + * inputs["cp_hot"] ** (1 / 3) + / inputs["k_hot"] ** (4 / 3) + ) + ) + J["Nu_dh_hot", "j_hot"] = ( + inputs["Re_dh_hot"] * inputs["mu_hot"] ** (1 / 3) * inputs["cp_hot"] ** (1 / 3) / inputs["k_hot"] ** (1 / 3) + ) + J["Nu_dh_hot", "Re_dh_hot"] = ( + inputs["j_hot"] * inputs["mu_hot"] ** (1 / 3) * inputs["cp_hot"] ** (1 / 3) / inputs["k_hot"] ** (1 / 3) + ) + class ConvectiveCoefficient(ExplicitComponent): """ @@ -507,38 +658,40 @@ class ConvectiveCoefficient(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('Nu_dh_cold', shape=(nn,)) - self.add_input('dh_cold', units='m') - self.add_input('k_cold', units='W/m/K') - self.add_input('Nu_dh_hot', shape=(nn,)) - self.add_input('dh_hot', units='m') - self.add_input('k_hot', units='W/m/K') - - self.add_output('h_conv_cold', shape=(nn,), units='W/m**2/K', lower=1.0) - self.add_output('h_conv_hot', shape=(nn,), units='W/m**2/K') + nn = self.options["num_nodes"] + self.add_input("Nu_dh_cold", shape=(nn,)) + self.add_input("dh_cold", units="m") + self.add_input("k_cold", units="W/m/K") + self.add_input("Nu_dh_hot", shape=(nn,)) + self.add_input("dh_hot", units="m") + self.add_input("k_hot", units="W/m/K") + + self.add_output("h_conv_cold", shape=(nn,), units="W/m**2/K", lower=1.0) + self.add_output("h_conv_hot", shape=(nn,), units="W/m**2/K") arange = np.arange(0, nn) - self.declare_partials(['h_conv_cold'], ['Nu_dh_cold'], rows=arange, cols=arange) - self.declare_partials(['h_conv_cold'], ['dh_cold','k_cold'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) - self.declare_partials(['h_conv_hot'], ['Nu_dh_hot'], rows=arange, cols=arange) - self.declare_partials(['h_conv_hot'], ['dh_hot','k_hot'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["h_conv_cold"], ["Nu_dh_cold"], rows=arange, cols=arange) + self.declare_partials(["h_conv_cold"], ["dh_cold", "k_cold"], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["h_conv_hot"], ["Nu_dh_hot"], rows=arange, cols=arange) + self.declare_partials(["h_conv_hot"], ["dh_hot", "k_hot"], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) def compute(self, inputs, outputs): - outputs['h_conv_cold'] = inputs['Nu_dh_cold'] * inputs['k_cold'] / inputs['dh_cold'] - outputs['h_conv_hot'] = inputs['Nu_dh_hot'] * inputs['k_hot'] / inputs['dh_hot'] - if np.min(outputs['h_conv_cold']) <= 0.0: + outputs["h_conv_cold"] = inputs["Nu_dh_cold"] * inputs["k_cold"] / inputs["dh_cold"] + outputs["h_conv_hot"] = inputs["Nu_dh_hot"] * inputs["k_hot"] / inputs["dh_hot"] + if np.min(outputs["h_conv_cold"]) <= 0.0: raise ValueError(self.msginfo) + def compute_partials(self, inputs, J): - J['h_conv_cold', 'Nu_dh_cold'] = inputs['k_cold'] / inputs['dh_cold'] - J['h_conv_cold', 'k_cold'] = inputs['Nu_dh_cold'] / inputs['dh_cold'] - J['h_conv_cold', 'dh_cold'] = - inputs['Nu_dh_cold'] * inputs['k_cold'] / inputs['dh_cold'] ** 2 + J["h_conv_cold", "Nu_dh_cold"] = inputs["k_cold"] / inputs["dh_cold"] + J["h_conv_cold", "k_cold"] = inputs["Nu_dh_cold"] / inputs["dh_cold"] + J["h_conv_cold", "dh_cold"] = -inputs["Nu_dh_cold"] * inputs["k_cold"] / inputs["dh_cold"] ** 2 + + J["h_conv_hot", "Nu_dh_hot"] = inputs["k_hot"] / inputs["dh_hot"] + J["h_conv_hot", "k_hot"] = inputs["Nu_dh_hot"] / inputs["dh_hot"] + J["h_conv_hot", "dh_hot"] = -inputs["Nu_dh_hot"] * inputs["k_hot"] / inputs["dh_hot"] ** 2 - J['h_conv_hot', 'Nu_dh_hot'] = inputs['k_hot'] / inputs['dh_hot'] - J['h_conv_hot', 'k_hot'] = inputs['Nu_dh_hot'] / inputs['dh_hot'] - J['h_conv_hot', 'dh_hot'] = - inputs['Nu_dh_hot'] * inputs['k_hot'] / inputs['dh_hot'] ** 2 class FinEfficiency(ExplicitComponent): """ @@ -583,97 +736,106 @@ class FinEfficiency(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('h_conv_cold', shape=(nn,), units='W/m**2/K') - self.add_input('fin_area_ratio_cold') - self.add_input('channel_height_cold', units='m') - self.add_input('h_conv_hot', shape=(nn,), units='W/m**2/K') - self.add_input('fin_area_ratio_hot') - self.add_input('channel_height_hot', units='m') - self.add_input('fin_thickness', units='m') - self.add_input('material_k', units='W/m/K') - - self.add_output('eta_overall_cold', shape=(nn,)) - self.add_output('eta_overall_hot', shape=(nn,)) + nn = self.options["num_nodes"] + self.add_input("h_conv_cold", shape=(nn,), units="W/m**2/K") + self.add_input("fin_area_ratio_cold") + self.add_input("channel_height_cold", units="m") + self.add_input("h_conv_hot", shape=(nn,), units="W/m**2/K") + self.add_input("fin_area_ratio_hot") + self.add_input("channel_height_hot", units="m") + self.add_input("fin_thickness", units="m") + self.add_input("material_k", units="W/m/K") + + self.add_output("eta_overall_cold", shape=(nn,)) + self.add_output("eta_overall_hot", shape=(nn,)) arange = np.arange(0, nn) - self.declare_partials(['eta_overall_cold'], ['h_conv_cold'], rows=arange, cols=arange) - self.declare_partials(['eta_overall_cold'], ['fin_area_ratio_cold','channel_height_cold', - 'fin_thickness','material_k'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) - self.declare_partials(['eta_overall_hot'], ['h_conv_hot'], rows=arange, cols=arange) - self.declare_partials(['eta_overall_hot'], ['fin_area_ratio_hot','channel_height_hot', - 'fin_thickness','material_k'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["eta_overall_cold"], ["h_conv_cold"], rows=arange, cols=arange) + self.declare_partials( + ["eta_overall_cold"], + ["fin_area_ratio_cold", "channel_height_cold", "fin_thickness", "material_k"], + rows=arange, + cols=np.zeros((nn,), dtype=np.int32), + ) + self.declare_partials(["eta_overall_hot"], ["h_conv_hot"], rows=arange, cols=arange) + self.declare_partials( + ["eta_overall_hot"], + ["fin_area_ratio_hot", "channel_height_hot", "fin_thickness", "material_k"], + rows=arange, + cols=np.zeros((nn,), dtype=np.int32), + ) def compute(self, inputs, outputs): - k = inputs['material_k'] - t_f = inputs['fin_thickness'] + k = inputs["material_k"] + t_f = inputs["fin_thickness"] - l_f_c = inputs['channel_height_cold'] - h_c = inputs['h_conv_cold'] + l_f_c = inputs["channel_height_cold"] + h_c = inputs["h_conv_cold"] m_cold = np.sqrt(2 * h_c / k / t_f) if np.min(h_c) <= 0.0: raise ValueError(self.msginfo) - eta_f_cold = 2* np.tanh(m_cold * l_f_c / 2) / m_cold / l_f_c - outputs['eta_overall_cold'] = 1 - inputs['fin_area_ratio_cold'] * (1 - eta_f_cold) + eta_f_cold = 2 * np.tanh(m_cold * l_f_c / 2) / m_cold / l_f_c + outputs["eta_overall_cold"] = 1 - inputs["fin_area_ratio_cold"] * (1 - eta_f_cold) - l_f_h = inputs['channel_height_hot'] - h_h = inputs['h_conv_hot'] + l_f_h = inputs["channel_height_hot"] + h_h = inputs["h_conv_hot"] m_hot = np.sqrt(2 * h_h / k / t_f) - eta_f_hot = 2* np.tanh(m_hot * l_f_h / 2) / m_hot / l_f_h - outputs['eta_overall_hot'] = 1 - inputs['fin_area_ratio_hot'] * (1 - eta_f_hot) + eta_f_hot = 2 * np.tanh(m_hot * l_f_h / 2) / m_hot / l_f_h + outputs["eta_overall_hot"] = 1 - inputs["fin_area_ratio_hot"] * (1 - eta_f_hot) def compute_partials(self, inputs, J): # get some aliases for brevity - t_f = inputs['fin_thickness'] - k = inputs['material_k'] + t_f = inputs["fin_thickness"] + k = inputs["material_k"] - l_f_c = inputs['channel_height_cold'] - h_c = inputs['h_conv_cold'] - afa_c = inputs['fin_area_ratio_cold'] + l_f_c = inputs["channel_height_cold"] + h_c = inputs["h_conv_cold"] + afa_c = inputs["fin_area_ratio_cold"] m_cold = np.sqrt(2 * h_c / k / t_f) eta_f_cold = 2 * np.tanh(m_cold * l_f_c / 2) / m_cold / l_f_c # compute partials of m with respect to its inputs - dmdh_c = (2 * h_c * t_f * k) ** (-1/2) - dmdt_c = -(h_c / t_f ** 3 / k / 2) ** (1/2) - dmdk_c = -(h_c / k ** 3 / t_f / 2) ** (1/2) + dmdh_c = (2 * h_c * t_f * k) ** (-1 / 2) + dmdt_c = -((h_c / t_f**3 / k / 2) ** (1 / 2)) + dmdk_c = -((h_c / k**3 / t_f / 2) ** (1 / 2)) # compute partials of fin efficiency with respect to its inputs ml_c = m_cold * l_f_c - deta_fdm_c = (ml_c * np.cosh(ml_c / 2) **-2 - 2 * np.tanh(ml_c / 2)) / ml_c / m_cold - deta_fdL_c = (ml_c * np.cosh(ml_c / 2) **-2 - 2 * np.tanh(ml_c / 2)) / ml_c / l_f_c + deta_fdm_c = (ml_c * np.cosh(ml_c / 2) ** -2 - 2 * np.tanh(ml_c / 2)) / ml_c / m_cold + deta_fdL_c = (ml_c * np.cosh(ml_c / 2) ** -2 - 2 * np.tanh(ml_c / 2)) / ml_c / l_f_c # compute partials with respect to overall efficiency - J['eta_overall_cold', 'fin_area_ratio_cold'] = eta_f_cold - 1 - J['eta_overall_cold', 'channel_height_cold'] = afa_c * deta_fdL_c - J['eta_overall_cold', 'fin_thickness'] = afa_c * deta_fdm_c * dmdt_c - J['eta_overall_cold', 'material_k'] = afa_c * deta_fdm_c * dmdk_c - J['eta_overall_cold', 'h_conv_cold'] = afa_c * deta_fdm_c * dmdh_c + J["eta_overall_cold", "fin_area_ratio_cold"] = eta_f_cold - 1 + J["eta_overall_cold", "channel_height_cold"] = afa_c * deta_fdL_c + J["eta_overall_cold", "fin_thickness"] = afa_c * deta_fdm_c * dmdt_c + J["eta_overall_cold", "material_k"] = afa_c * deta_fdm_c * dmdk_c + J["eta_overall_cold", "h_conv_cold"] = afa_c * deta_fdm_c * dmdh_c - l_f_h = inputs['channel_height_hot'] - h_h = inputs['h_conv_hot'] - afa_h = inputs['fin_area_ratio_hot'] + l_f_h = inputs["channel_height_hot"] + h_h = inputs["h_conv_hot"] + afa_h = inputs["fin_area_ratio_hot"] m_hot = np.sqrt(2 * h_h / k / t_f) eta_f_hot = 2 * np.tanh(m_hot * l_f_h / 2) / m_hot / l_f_h # compute partials of m with respect to its inputs - dmdh_h = (2 * h_h * t_f * k) ** (-1/2) - dmdt_h = -(h_h / t_f ** 3 / k / 2) ** (1/2) - dmdk_h = -(h_h / k ** 3 / t_f / 2) ** (1/2) + dmdh_h = (2 * h_h * t_f * k) ** (-1 / 2) + dmdt_h = -((h_h / t_f**3 / k / 2) ** (1 / 2)) + dmdk_h = -((h_h / k**3 / t_f / 2) ** (1 / 2)) # compute partials of fin efficiency with respect to its inputs ml_h = m_hot * l_f_h - deta_fdm_h = (ml_h * np.cosh(ml_h / 2) **-2 - 2 * np.tanh(ml_h / 2)) / ml_h / m_hot - deta_fdL_h = (ml_h * np.cosh(ml_h / 2) **-2 - 2 * np.tanh(ml_h / 2)) / ml_h / l_f_h + deta_fdm_h = (ml_h * np.cosh(ml_h / 2) ** -2 - 2 * np.tanh(ml_h / 2)) / ml_h / m_hot + deta_fdL_h = (ml_h * np.cosh(ml_h / 2) ** -2 - 2 * np.tanh(ml_h / 2)) / ml_h / l_f_h # compute partials with respect to overall efficiency - J['eta_overall_hot', 'fin_area_ratio_hot'] = eta_f_hot - 1 - J['eta_overall_hot', 'channel_height_hot'] = afa_h * deta_fdL_h - J['eta_overall_hot', 'fin_thickness'] = afa_h * deta_fdm_h * dmdt_h - J['eta_overall_hot', 'material_k'] = afa_h * deta_fdm_h * dmdk_h - J['eta_overall_hot', 'h_conv_hot'] = afa_h * deta_fdm_h * dmdh_h + J["eta_overall_hot", "fin_area_ratio_hot"] = eta_f_hot - 1 + J["eta_overall_hot", "channel_height_hot"] = afa_h * deta_fdL_h + J["eta_overall_hot", "fin_thickness"] = afa_h * deta_fdm_h * dmdt_h + J["eta_overall_hot", "material_k"] = afa_h * deta_fdm_h * dmdk_h + J["eta_overall_hot", "h_conv_hot"] = afa_h * deta_fdm_h * dmdh_h + class UAOverall(ExplicitComponent): """ @@ -712,59 +874,72 @@ class UAOverall(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('fouling_factor_cold', default=0.0) - self.options.declare('fouling_factor_hot', default=0.0) + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("fouling_factor_cold", default=0.0) + self.options.declare("fouling_factor_hot", default=0.0) def setup(self): - nn = self.options['num_nodes'] - self.add_input('h_conv_cold', shape=(nn,), units='W/m**2/K') - self.add_input('heat_transfer_area_cold', units='m**2') - self.add_input('eta_overall_cold', shape=(nn,)) - self.add_input('h_conv_hot', shape=(nn,), units='W/m**2/K') - self.add_input('heat_transfer_area_hot', units='m**2') - self.add_input('eta_overall_hot', shape=(nn,)) - - self.add_output('UA_overall', shape=(nn,), units='W/K') + nn = self.options["num_nodes"] + self.add_input("h_conv_cold", shape=(nn,), units="W/m**2/K") + self.add_input("heat_transfer_area_cold", units="m**2") + self.add_input("eta_overall_cold", shape=(nn,)) + self.add_input("h_conv_hot", shape=(nn,), units="W/m**2/K") + self.add_input("heat_transfer_area_hot", units="m**2") + self.add_input("eta_overall_hot", shape=(nn,)) + + self.add_output("UA_overall", shape=(nn,), units="W/K") arange = np.arange(0, nn) - self.declare_partials(['UA_overall'], - ['h_conv_cold', 'eta_overall_cold', 'h_conv_hot', 'eta_overall_hot'], - rows=arange, cols=arange) - self.declare_partials(['UA_overall'], - ['heat_transfer_area_cold', 'heat_transfer_area_hot'], - rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials( + ["UA_overall"], + ["h_conv_cold", "eta_overall_cold", "h_conv_hot", "eta_overall_hot"], + rows=arange, + cols=arange, + ) + self.declare_partials( + ["UA_overall"], + ["heat_transfer_area_cold", "heat_transfer_area_hot"], + rows=arange, + cols=np.zeros((nn,), dtype=np.int32), + ) def compute(self, inputs, outputs): - Rc = 1 / inputs['h_conv_cold'] / inputs['heat_transfer_area_cold'] / inputs['eta_overall_cold'] - Rfc = self.options['fouling_factor_cold'] / inputs['heat_transfer_area_cold'] / inputs['eta_overall_cold'] - Rh = 1 / inputs['h_conv_hot'] / inputs['heat_transfer_area_hot'] / inputs['eta_overall_hot'] - Rfc = self.options['fouling_factor_hot'] / inputs['heat_transfer_area_hot'] / inputs['eta_overall_hot'] - outputs['UA_overall'] = 1/(Rc + Rfc + Rh + Rfc) + Rc = 1 / inputs["h_conv_cold"] / inputs["heat_transfer_area_cold"] / inputs["eta_overall_cold"] + Rfc = self.options["fouling_factor_cold"] / inputs["heat_transfer_area_cold"] / inputs["eta_overall_cold"] + Rh = 1 / inputs["h_conv_hot"] / inputs["heat_transfer_area_hot"] / inputs["eta_overall_hot"] + Rfc = self.options["fouling_factor_hot"] / inputs["heat_transfer_area_hot"] / inputs["eta_overall_hot"] + outputs["UA_overall"] = 1 / (Rc + Rfc + Rh + Rfc) def compute_partials(self, inputs, J): - Rc = 1 / inputs['h_conv_cold'] / inputs['heat_transfer_area_cold'] / inputs['eta_overall_cold'] - Rfc = self.options['fouling_factor_cold'] / inputs['heat_transfer_area_cold'] / inputs['eta_overall_cold'] - Rh = 1 / inputs['h_conv_hot'] / inputs['heat_transfer_area_hot'] / inputs['eta_overall_hot'] - Rfc = self.options['fouling_factor_hot'] / inputs['heat_transfer_area_hot'] / inputs['eta_overall_hot'] - - dRcdh = - 1 / inputs['h_conv_cold']**2 / inputs['heat_transfer_area_cold'] / inputs['eta_overall_cold'] - dRcdA = - 1 / inputs['h_conv_cold'] / inputs['heat_transfer_area_cold']**2 / inputs['eta_overall_cold'] - dRcdeta = - 1 / inputs['h_conv_cold'] / inputs['heat_transfer_area_cold'] / inputs['eta_overall_cold'] **2 - dRfcdA = - self.options['fouling_factor_cold'] / inputs['heat_transfer_area_cold']**2 / inputs['eta_overall_cold'] - dRfcdeta = - self.options['fouling_factor_cold'] / inputs['heat_transfer_area_cold'] / inputs['eta_overall_cold'] **2 - - dRhdh = - 1 / inputs['h_conv_hot']**2 / inputs['heat_transfer_area_hot'] / inputs['eta_overall_hot'] - dRhdA = - 1 / inputs['h_conv_hot'] / inputs['heat_transfer_area_hot']**2 / inputs['eta_overall_hot'] - dRhdeta = - 1 / inputs['h_conv_hot'] / inputs['heat_transfer_area_hot'] / inputs['eta_overall_hot'] **2 - dRfhdA = - self.options['fouling_factor_hot'] / inputs['heat_transfer_area_hot']**2 / inputs['eta_overall_hot'] - dRfhdeta = - self.options['fouling_factor_hot'] / inputs['heat_transfer_area_hot'] / inputs['eta_overall_hot'] **2 - - J['UA_overall', 'h_conv_cold'] = -1/(Rc + Rfc + Rh + Rfc)**2 * (dRcdh) - J['UA_overall', 'heat_transfer_area_cold'] = -1/(Rc + Rfc + Rh + Rfc)**2 * (dRcdA + dRfcdA) - J['UA_overall', 'eta_overall_cold'] = -1/(Rc + Rfc + Rh + Rfc)**2 * (dRcdeta + dRfcdeta) - J['UA_overall', 'h_conv_hot'] = -1/(Rc + Rfc + Rh + Rfc)**2 * (dRhdh) - J['UA_overall', 'heat_transfer_area_hot'] = -1/(Rc + Rfc + Rh + Rfc)**2 * (dRhdA + dRfhdA) - J['UA_overall', 'eta_overall_hot'] = -1/(Rc + Rfc + Rh + Rfc)**2 * (dRhdeta + dRfhdeta) + Rc = 1 / inputs["h_conv_cold"] / inputs["heat_transfer_area_cold"] / inputs["eta_overall_cold"] + Rfc = self.options["fouling_factor_cold"] / inputs["heat_transfer_area_cold"] / inputs["eta_overall_cold"] + Rh = 1 / inputs["h_conv_hot"] / inputs["heat_transfer_area_hot"] / inputs["eta_overall_hot"] + Rfc = self.options["fouling_factor_hot"] / inputs["heat_transfer_area_hot"] / inputs["eta_overall_hot"] + + dRcdh = -1 / inputs["h_conv_cold"] ** 2 / inputs["heat_transfer_area_cold"] / inputs["eta_overall_cold"] + dRcdA = -1 / inputs["h_conv_cold"] / inputs["heat_transfer_area_cold"] ** 2 / inputs["eta_overall_cold"] + dRcdeta = -1 / inputs["h_conv_cold"] / inputs["heat_transfer_area_cold"] / inputs["eta_overall_cold"] ** 2 + dRfcdA = ( + -self.options["fouling_factor_cold"] / inputs["heat_transfer_area_cold"] ** 2 / inputs["eta_overall_cold"] + ) + dRfcdeta = ( + -self.options["fouling_factor_cold"] / inputs["heat_transfer_area_cold"] / inputs["eta_overall_cold"] ** 2 + ) + + dRhdh = -1 / inputs["h_conv_hot"] ** 2 / inputs["heat_transfer_area_hot"] / inputs["eta_overall_hot"] + dRhdA = -1 / inputs["h_conv_hot"] / inputs["heat_transfer_area_hot"] ** 2 / inputs["eta_overall_hot"] + dRhdeta = -1 / inputs["h_conv_hot"] / inputs["heat_transfer_area_hot"] / inputs["eta_overall_hot"] ** 2 + dRfhdA = -self.options["fouling_factor_hot"] / inputs["heat_transfer_area_hot"] ** 2 / inputs["eta_overall_hot"] + dRfhdeta = ( + -self.options["fouling_factor_hot"] / inputs["heat_transfer_area_hot"] / inputs["eta_overall_hot"] ** 2 + ) + + J["UA_overall", "h_conv_cold"] = -1 / (Rc + Rfc + Rh + Rfc) ** 2 * (dRcdh) + J["UA_overall", "heat_transfer_area_cold"] = -1 / (Rc + Rfc + Rh + Rfc) ** 2 * (dRcdA + dRfcdA) + J["UA_overall", "eta_overall_cold"] = -1 / (Rc + Rfc + Rh + Rfc) ** 2 * (dRcdeta + dRfcdeta) + J["UA_overall", "h_conv_hot"] = -1 / (Rc + Rfc + Rh + Rfc) ** 2 * (dRhdh) + J["UA_overall", "heat_transfer_area_hot"] = -1 / (Rc + Rfc + Rh + Rfc) ** 2 * (dRhdA + dRfhdA) + J["UA_overall", "eta_overall_hot"] = -1 / (Rc + Rfc + Rh + Rfc) ** 2 * (dRhdeta + dRfhdeta) + class NTUMethod(ExplicitComponent): """ @@ -805,80 +980,77 @@ class NTUMethod(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('UA_overall', shape=(nn,), units='W/K') - self.add_input('mdot_cold', shape=(nn,), units='kg/s') - self.add_input('T_in_cold', shape=(nn,), units='K') - self.add_input('cp_cold', units='J/kg/K') - self.add_input('mdot_hot', shape=(nn,), units='kg/s') - self.add_input('T_in_hot', shape=(nn,), units='K') - self.add_input('cp_hot', units='J/kg/K') - - self.add_output('NTU', shape=(nn,), lower=0.1) - self.add_output('heat_max', shape=(nn,), units='W') - self.add_output('C_ratio', shape=(nn,)) + nn = self.options["num_nodes"] + self.add_input("UA_overall", shape=(nn,), units="W/K") + self.add_input("mdot_cold", shape=(nn,), units="kg/s") + self.add_input("T_in_cold", shape=(nn,), units="K") + self.add_input("cp_cold", units="J/kg/K") + self.add_input("mdot_hot", shape=(nn,), units="kg/s") + self.add_input("T_in_hot", shape=(nn,), units="K") + self.add_input("cp_hot", units="J/kg/K") + + self.add_output("NTU", shape=(nn,), lower=0.1) + self.add_output("heat_max", shape=(nn,), units="W") + self.add_output("C_ratio", shape=(nn,)) arange = np.arange(0, nn) - self.declare_partials(['NTU'], - ['UA_overall', 'mdot_cold', 'mdot_hot'], - rows=arange, cols=arange) - self.declare_partials(['C_ratio'], - ['mdot_cold', 'mdot_hot'], - rows=arange, cols=arange) - self.declare_partials(['heat_max'], - ['mdot_cold', 'mdot_hot', 'T_in_cold', 'T_in_hot'], - rows=arange, cols=arange) - self.declare_partials(['heat_max', 'NTU', 'C_ratio'], - ['cp_cold', 'cp_hot'], - rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["NTU"], ["UA_overall", "mdot_cold", "mdot_hot"], rows=arange, cols=arange) + self.declare_partials(["C_ratio"], ["mdot_cold", "mdot_hot"], rows=arange, cols=arange) + self.declare_partials( + ["heat_max"], ["mdot_cold", "mdot_hot", "T_in_cold", "T_in_hot"], rows=arange, cols=arange + ) + self.declare_partials( + ["heat_max", "NTU", "C_ratio"], ["cp_cold", "cp_hot"], rows=arange, cols=np.zeros((nn,), dtype=np.int32) + ) def compute(self, inputs, outputs): - C_cold = inputs['mdot_cold'] * inputs['cp_cold'] - C_hot = inputs['mdot_hot'] * inputs['cp_hot'] + C_cold = inputs["mdot_cold"] * inputs["cp_cold"] + C_hot = inputs["mdot_hot"] * inputs["cp_hot"] C_min_bool = np.less(C_cold, C_hot) C_min = np.where(C_min_bool, C_cold, C_hot) C_max = np.where(C_min_bool, C_hot, C_cold) - outputs['NTU'] = inputs['UA_overall'] / C_min - outputs['heat_max'] = (inputs['T_in_hot'] - inputs['T_in_cold']) * C_min - outputs['C_ratio'] = C_min / C_max + outputs["NTU"] = inputs["UA_overall"] / C_min + outputs["heat_max"] = (inputs["T_in_hot"] - inputs["T_in_cold"]) * C_min + outputs["C_ratio"] = C_min / C_max def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - C_cold = inputs['mdot_cold'] * inputs['cp_cold'] - C_hot = inputs['mdot_hot'] * inputs['cp_hot'] + nn = self.options["num_nodes"] + C_cold = inputs["mdot_cold"] * inputs["cp_cold"] + C_hot = inputs["mdot_hot"] * inputs["cp_hot"] C_min_bool = np.less(C_cold, C_hot) C_min = np.where(C_min_bool, C_cold, C_hot) C_max = np.where(C_min_bool, C_hot, C_cold) - dCmindmdotcold = np.where(C_min_bool, inputs['cp_cold']*np.ones((nn,)), np.zeros((nn,))) - dCmindcpcold = np.where(C_min_bool, inputs['mdot_cold'], np.zeros((nn,))) - dCmindmdothot = np.where(C_min_bool, np.zeros((nn,)), inputs['cp_hot']*np.ones((nn,))) - dCmindcphot = np.where(C_min_bool, np.zeros((nn,)), inputs['mdot_hot']) - - dCmaxdmdotcold = np.where(C_min_bool, np.zeros((nn,)), inputs['cp_cold']*np.ones((nn,))) - dCmaxdcpcold = np.where(C_min_bool, np.zeros((nn,)), inputs['mdot_cold']) - dCmaxdmdothot = np.where(C_min_bool, inputs['cp_hot']*np.ones((nn,)), np.zeros((nn,))) - dCmaxdcphot = np.where(C_min_bool, inputs['mdot_hot'], np.zeros((nn,))) - - J['NTU', 'UA_overall'] = 1 / C_min - J['NTU', 'mdot_cold'] = -inputs['UA_overall'] / C_min ** 2 * dCmindmdotcold - J['NTU', 'cp_cold'] = -inputs['UA_overall'] / C_min ** 2 * dCmindcpcold - J['NTU', 'mdot_hot'] = -inputs['UA_overall'] / C_min ** 2 * dCmindmdothot - J['NTU', 'cp_hot'] = -inputs['UA_overall'] / C_min ** 2 * dCmindcphot - - J['heat_max', 'T_in_cold'] = - C_min - J['heat_max', 'T_in_hot'] = C_min - J['heat_max', 'mdot_cold'] = (inputs['T_in_hot']-inputs['T_in_cold']) * dCmindmdotcold - J['heat_max', 'cp_cold'] = (inputs['T_in_hot']-inputs['T_in_cold']) * dCmindcpcold - J['heat_max', 'mdot_hot'] = (inputs['T_in_hot']-inputs['T_in_cold']) * dCmindmdothot - J['heat_max', 'cp_hot'] = (inputs['T_in_hot']-inputs['T_in_cold']) * dCmindcphot - - J['C_ratio', 'mdot_cold'] = (dCmindmdotcold * C_max - dCmaxdmdotcold * C_min) / C_max **2 - J['C_ratio','cp_cold'] = (dCmindcpcold * C_max - dCmaxdcpcold * C_min) / C_max **2 - J['C_ratio', 'mdot_hot'] = (dCmindmdothot * C_max - dCmaxdmdothot * C_min) / C_max **2 - J['C_ratio','cp_hot'] = (dCmindcphot * C_max - dCmaxdcphot * C_min) / C_max **2 + dCmindmdotcold = np.where(C_min_bool, inputs["cp_cold"] * np.ones((nn,)), np.zeros((nn,))) + dCmindcpcold = np.where(C_min_bool, inputs["mdot_cold"], np.zeros((nn,))) + dCmindmdothot = np.where(C_min_bool, np.zeros((nn,)), inputs["cp_hot"] * np.ones((nn,))) + dCmindcphot = np.where(C_min_bool, np.zeros((nn,)), inputs["mdot_hot"]) + + dCmaxdmdotcold = np.where(C_min_bool, np.zeros((nn,)), inputs["cp_cold"] * np.ones((nn,))) + dCmaxdcpcold = np.where(C_min_bool, np.zeros((nn,)), inputs["mdot_cold"]) + dCmaxdmdothot = np.where(C_min_bool, inputs["cp_hot"] * np.ones((nn,)), np.zeros((nn,))) + dCmaxdcphot = np.where(C_min_bool, inputs["mdot_hot"], np.zeros((nn,))) + + J["NTU", "UA_overall"] = 1 / C_min + J["NTU", "mdot_cold"] = -inputs["UA_overall"] / C_min**2 * dCmindmdotcold + J["NTU", "cp_cold"] = -inputs["UA_overall"] / C_min**2 * dCmindcpcold + J["NTU", "mdot_hot"] = -inputs["UA_overall"] / C_min**2 * dCmindmdothot + J["NTU", "cp_hot"] = -inputs["UA_overall"] / C_min**2 * dCmindcphot + + J["heat_max", "T_in_cold"] = -C_min + J["heat_max", "T_in_hot"] = C_min + J["heat_max", "mdot_cold"] = (inputs["T_in_hot"] - inputs["T_in_cold"]) * dCmindmdotcold + J["heat_max", "cp_cold"] = (inputs["T_in_hot"] - inputs["T_in_cold"]) * dCmindcpcold + J["heat_max", "mdot_hot"] = (inputs["T_in_hot"] - inputs["T_in_cold"]) * dCmindmdothot + J["heat_max", "cp_hot"] = (inputs["T_in_hot"] - inputs["T_in_cold"]) * dCmindcphot + + J["C_ratio", "mdot_cold"] = (dCmindmdotcold * C_max - dCmaxdmdotcold * C_min) / C_max**2 + J["C_ratio", "cp_cold"] = (dCmindcpcold * C_max - dCmaxdcpcold * C_min) / C_max**2 + J["C_ratio", "mdot_hot"] = (dCmindmdothot * C_max - dCmaxdmdothot * C_min) / C_max**2 + J["C_ratio", "cp_hot"] = (dCmindcphot * C_max - dCmaxdcphot * C_min) / C_max**2 + class CrossFlowNTUEffectiveness(ExplicitComponent): """ @@ -905,31 +1077,34 @@ class CrossFlowNTUEffectiveness(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('NTU', shape=(nn,)) - self.add_input('C_ratio', shape=(nn,)) - self.add_output('effectiveness', shape=(nn,)) + nn = self.options["num_nodes"] + self.add_input("NTU", shape=(nn,)) + self.add_input("C_ratio", shape=(nn,)) + self.add_output("effectiveness", shape=(nn,)) arange = np.arange(0, nn) - self.declare_partials(['effectiveness'], - ['NTU', 'C_ratio'], - rows=arange, cols=arange) + self.declare_partials(["effectiveness"], ["NTU", "C_ratio"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - Cr = inputs['C_ratio'] - ntu = inputs['NTU'] - outputs['effectiveness'] = 1 - np.exp(ntu**0.22/Cr*(np.exp(-Cr*ntu**0.78)-1)) - + nn = self.options["num_nodes"] + Cr = inputs["C_ratio"] + ntu = inputs["NTU"] + outputs["effectiveness"] = 1 - np.exp(ntu**0.22 / Cr * (np.exp(-Cr * ntu**0.78) - 1)) def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - Cr = inputs['C_ratio'] - ntu = inputs['NTU'] - J['effectiveness','C_ratio'] = -np.exp((ntu** 0.22 * (np.exp(-Cr* ntu** 0.78) - 1))/Cr) * (-(ntu** 0.22 * (np.exp(-Cr * ntu** 0.78) - 1))/Cr** 2 - (ntu*np.exp(-Cr*ntu** 0.78))/Cr) - J['effectiveness', 'NTU'] = (39*Cr*ntu** 0.78 * np.exp((ntu** 0.22 * (np.exp(-Cr* ntu** 0.78) - 1))/Cr - Cr * ntu** 0.78) - 11 * np.exp((ntu** 0.22 * (np.exp(-Cr * ntu** 0.78) - 1))/Cr) * (np.exp(-Cr * ntu** 0.78) - 1))/(50*Cr*ntu** 0.78) + nn = self.options["num_nodes"] + Cr = inputs["C_ratio"] + ntu = inputs["NTU"] + J["effectiveness", "C_ratio"] = -np.exp((ntu**0.22 * (np.exp(-Cr * ntu**0.78) - 1)) / Cr) * ( + -(ntu**0.22 * (np.exp(-Cr * ntu**0.78) - 1)) / Cr**2 - (ntu * np.exp(-Cr * ntu**0.78)) / Cr + ) + J["effectiveness", "NTU"] = ( + 39 * Cr * ntu**0.78 * np.exp((ntu**0.22 * (np.exp(-Cr * ntu**0.78) - 1)) / Cr - Cr * ntu**0.78) + - 11 * np.exp((ntu**0.22 * (np.exp(-Cr * ntu**0.78) - 1)) / Cr) * (np.exp(-Cr * ntu**0.78) - 1) + ) / (50 * Cr * ntu**0.78) + class NTUEffectivenessActualHeatTransfer(ExplicitComponent): """ @@ -956,26 +1131,24 @@ class NTUEffectivenessActualHeatTransfer(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('effectiveness', shape=(nn,)) - self.add_input('heat_max', shape=(nn,), units='W') - self.add_output('heat_transfer', shape=(nn,), units='W') + nn = self.options["num_nodes"] + self.add_input("effectiveness", shape=(nn,)) + self.add_input("heat_max", shape=(nn,), units="W") + self.add_output("heat_transfer", shape=(nn,), units="W") arange = np.arange(0, nn) - self.declare_partials(['heat_transfer'], - ['effectiveness', 'heat_max'], - rows=arange, cols=arange) + self.declare_partials(["heat_transfer"], ["effectiveness", "heat_max"], rows=arange, cols=arange) def compute(self, inputs, outputs): - outputs['heat_transfer'] = inputs['effectiveness'] * inputs['heat_max'] - + outputs["heat_transfer"] = inputs["effectiveness"] * inputs["heat_max"] def compute_partials(self, inputs, J): - J['heat_transfer', 'effectiveness'] = inputs['heat_max'] - J['heat_transfer', 'heat_max'] = inputs['effectiveness'] + J["heat_transfer", "effectiveness"] = inputs["heat_max"] + J["heat_transfer", "heat_max"] = inputs["effectiveness"] + class OutletTemperatures(ExplicitComponent): """ @@ -1013,47 +1186,40 @@ class OutletTemperatures(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") def setup(self): - nn = self.options['num_nodes'] - self.add_input('heat_transfer', shape=(nn,), units='W') - self.add_input('mdot_cold', shape=(nn,), units='kg/s') - self.add_input('T_in_cold', shape=(nn,), units='K') - self.add_input('cp_cold', units='J/kg/K') - self.add_input('mdot_hot', shape=(nn,), units='kg/s') - self.add_input('T_in_hot', shape=(nn,), units='K') - self.add_input('cp_hot', units='J/kg/K') - self.add_output('T_out_cold', shape=(nn,), units='K') - self.add_output('T_out_hot', shape=(nn,), units='K') + nn = self.options["num_nodes"] + self.add_input("heat_transfer", shape=(nn,), units="W") + self.add_input("mdot_cold", shape=(nn,), units="kg/s") + self.add_input("T_in_cold", shape=(nn,), units="K") + self.add_input("cp_cold", units="J/kg/K") + self.add_input("mdot_hot", shape=(nn,), units="kg/s") + self.add_input("T_in_hot", shape=(nn,), units="K") + self.add_input("cp_hot", units="J/kg/K") + self.add_output("T_out_cold", shape=(nn,), units="K") + self.add_output("T_out_hot", shape=(nn,), units="K") arange = np.arange(0, nn) - self.declare_partials(['T_out_cold'], - ['heat_transfer', 'mdot_cold'], - rows=arange, cols=arange) - self.declare_partials(['T_out_cold'], - ['T_in_cold'], - rows=arange, cols=arange, val=np.ones((nn,))) - self.declare_partials(['T_out_hot'], - ['heat_transfer', 'mdot_hot'], - rows=arange, cols=arange) - self.declare_partials(['T_out_hot'], - ['T_in_hot'], - rows=arange, cols=arange, val=np.ones((nn,))) - self.declare_partials(['T_out_cold'], ['cp_cold'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) - self.declare_partials(['T_out_hot'], ['cp_hot'], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["T_out_cold"], ["heat_transfer", "mdot_cold"], rows=arange, cols=arange) + self.declare_partials(["T_out_cold"], ["T_in_cold"], rows=arange, cols=arange, val=np.ones((nn,))) + self.declare_partials(["T_out_hot"], ["heat_transfer", "mdot_hot"], rows=arange, cols=arange) + self.declare_partials(["T_out_hot"], ["T_in_hot"], rows=arange, cols=arange, val=np.ones((nn,))) + self.declare_partials(["T_out_cold"], ["cp_cold"], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["T_out_hot"], ["cp_hot"], rows=arange, cols=np.zeros((nn,), dtype=np.int32)) def compute(self, inputs, outputs): - outputs['T_out_cold'] = inputs['T_in_cold'] + inputs['heat_transfer'] / inputs['mdot_cold'] / inputs['cp_cold'] - outputs['T_out_hot'] = inputs['T_in_hot'] - inputs['heat_transfer'] / inputs['mdot_hot'] / inputs['cp_hot'] + outputs["T_out_cold"] = inputs["T_in_cold"] + inputs["heat_transfer"] / inputs["mdot_cold"] / inputs["cp_cold"] + outputs["T_out_hot"] = inputs["T_in_hot"] - inputs["heat_transfer"] / inputs["mdot_hot"] / inputs["cp_hot"] def compute_partials(self, inputs, J): - J['T_out_cold', 'heat_transfer'] = 1 / inputs['mdot_cold'] / inputs['cp_cold'] - J['T_out_cold', 'mdot_cold'] = - inputs['heat_transfer'] / inputs['mdot_cold']**2 / inputs['cp_cold'] - J['T_out_cold', 'cp_cold'] = - inputs['heat_transfer'] / inputs['mdot_cold'] / inputs['cp_cold'] ** 2 - J['T_out_hot', 'heat_transfer'] = - 1 / inputs['mdot_hot'] / inputs['cp_hot'] - J['T_out_hot', 'mdot_hot'] = inputs['heat_transfer'] / inputs['mdot_hot']**2 / inputs['cp_hot'] - J['T_out_hot', 'cp_hot'] = inputs['heat_transfer'] / inputs['mdot_hot'] / inputs['cp_hot'] ** 2 + J["T_out_cold", "heat_transfer"] = 1 / inputs["mdot_cold"] / inputs["cp_cold"] + J["T_out_cold", "mdot_cold"] = -inputs["heat_transfer"] / inputs["mdot_cold"] ** 2 / inputs["cp_cold"] + J["T_out_cold", "cp_cold"] = -inputs["heat_transfer"] / inputs["mdot_cold"] / inputs["cp_cold"] ** 2 + J["T_out_hot", "heat_transfer"] = -1 / inputs["mdot_hot"] / inputs["cp_hot"] + J["T_out_hot", "mdot_hot"] = inputs["heat_transfer"] / inputs["mdot_hot"] ** 2 / inputs["cp_hot"] + J["T_out_hot", "cp_hot"] = inputs["heat_transfer"] / inputs["mdot_hot"] / inputs["cp_hot"] ** 2 + class PressureDrop(ExplicitComponent): """ @@ -1111,78 +1277,94 @@ class PressureDrop(ExplicitComponent): """ def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('Kc_cold', default=0.3, desc='Irreversible contraction loss coefficient') - self.options.declare('Ke_cold', default=-0.1, desc='Irreversible contraction loss coefficient') - self.options.declare('Kc_hot', default=0.3, desc='Irreversible contraction loss coefficient') - self.options.declare('Ke_hot', default=-0.1, desc='Irreversible contraction loss coefficient') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("Kc_cold", default=0.3, desc="Irreversible contraction loss coefficient") + self.options.declare("Ke_cold", default=-0.1, desc="Irreversible contraction loss coefficient") + self.options.declare("Kc_hot", default=0.3, desc="Irreversible contraction loss coefficient") + self.options.declare("Ke_hot", default=-0.1, desc="Irreversible contraction loss coefficient") def setup(self): - nn = self.options['num_nodes'] - self.add_input('length_overall', units='m') - self.add_input('width_overall', units='m') + nn = self.options["num_nodes"] + self.add_input("length_overall", units="m") + self.add_input("width_overall", units="m") - self.add_input('f_cold', shape=(nn,)) - self.add_input('mdot_cold', shape=(nn,), units='kg/s') - self.add_input('rho_cold', shape=(nn,), units='kg/m**3') - self.add_input('dh_cold', units='m') - self.add_input('xs_area_cold', units='m**2') + self.add_input("f_cold", shape=(nn,)) + self.add_input("mdot_cold", shape=(nn,), units="kg/s") + self.add_input("rho_cold", shape=(nn,), units="kg/m**3") + self.add_input("dh_cold", units="m") + self.add_input("xs_area_cold", units="m**2") - self.add_input('f_hot', shape=(nn,)) - self.add_input('mdot_hot', shape=(nn,), units='kg/s') - self.add_input('rho_hot', shape=(nn,), units='kg/m**3') - self.add_input('dh_hot', units='m') - self.add_input('xs_area_hot', units='m**2') + self.add_input("f_hot", shape=(nn,)) + self.add_input("mdot_hot", shape=(nn,), units="kg/s") + self.add_input("rho_hot", shape=(nn,), units="kg/m**3") + self.add_input("dh_hot", units="m") + self.add_input("xs_area_hot", units="m**2") - self.add_output('delta_p_cold', shape=(nn,), units='Pa') - self.add_output('delta_p_hot', shape=(nn,), units='Pa') + self.add_output("delta_p_cold", shape=(nn,), units="Pa") + self.add_output("delta_p_hot", shape=(nn,), units="Pa") arange = np.arange(0, nn) - self.declare_partials(['delta_p_cold'], - ['mdot_cold', 'rho_cold','f_cold'], - rows=arange, cols=arange) - self.declare_partials(['delta_p_cold'], - ['xs_area_cold', 'length_overall','dh_cold'], - rows=arange, cols=np.zeros((nn,), dtype=np.int32)) - self.declare_partials(['delta_p_hot'], - ['mdot_hot', 'rho_hot','f_hot'], - rows=arange, cols=arange) - self.declare_partials(['delta_p_hot'], - ['xs_area_hot', 'width_overall','dh_hot'], - rows=arange, cols=np.zeros((nn,), dtype=np.int32)) + self.declare_partials(["delta_p_cold"], ["mdot_cold", "rho_cold", "f_cold"], rows=arange, cols=arange) + self.declare_partials( + ["delta_p_cold"], + ["xs_area_cold", "length_overall", "dh_cold"], + rows=arange, + cols=np.zeros((nn,), dtype=np.int32), + ) + self.declare_partials(["delta_p_hot"], ["mdot_hot", "rho_hot", "f_hot"], rows=arange, cols=arange) + self.declare_partials( + ["delta_p_hot"], + ["xs_area_hot", "width_overall", "dh_hot"], + rows=arange, + cols=np.zeros((nn,), dtype=np.int32), + ) + def compute(self, inputs, outputs): - dyn_press_cold = (1/2)*(inputs['mdot_cold']/inputs['xs_area_cold'])**2/inputs['rho_cold'] - dyn_press_hot = (1/2)*(inputs['mdot_hot']/inputs['xs_area_hot'])**2/inputs['rho_hot'] - Kec = self.options['Ke_cold'] - Kcc = self.options['Kc_cold'] - Keh = self.options['Ke_hot'] - Kch = self.options['Kc_hot'] - outputs['delta_p_cold'] = dyn_press_cold * (-Kec-Kcc-4*inputs['length_overall']*inputs['f_cold']/inputs['dh_cold']) - outputs['delta_p_hot'] = dyn_press_hot * (-Kec-Kcc-4*inputs['width_overall']*inputs['f_hot']/inputs['dh_hot']) + dyn_press_cold = (1 / 2) * (inputs["mdot_cold"] / inputs["xs_area_cold"]) ** 2 / inputs["rho_cold"] + dyn_press_hot = (1 / 2) * (inputs["mdot_hot"] / inputs["xs_area_hot"]) ** 2 / inputs["rho_hot"] + Kec = self.options["Ke_cold"] + Kcc = self.options["Kc_cold"] + Keh = self.options["Ke_hot"] + Kch = self.options["Kc_hot"] + outputs["delta_p_cold"] = dyn_press_cold * ( + -Kec - Kcc - 4 * inputs["length_overall"] * inputs["f_cold"] / inputs["dh_cold"] + ) + outputs["delta_p_hot"] = dyn_press_hot * ( + -Kec - Kcc - 4 * inputs["width_overall"] * inputs["f_hot"] / inputs["dh_hot"] + ) def compute_partials(self, inputs, J): - dyn_press_cold = (1/2)*(inputs['mdot_cold']/inputs['xs_area_cold'])**2/inputs['rho_cold'] - dyn_press_hot = (1/2)*(inputs['mdot_hot']/inputs['xs_area_hot'])**2/inputs['rho_hot'] - Kec = self.options['Ke_cold'] - Kcc = self.options['Kc_cold'] - Keh = self.options['Ke_hot'] - Kch = self.options['Kc_hot'] - losses_cold = (-Kec-Kcc-4*inputs['length_overall']*inputs['f_cold']/inputs['dh_cold']) - losses_hot = (-Kec-Kcc-4*inputs['width_overall']*inputs['f_hot']/inputs['dh_hot']) - - J['delta_p_cold', 'mdot_cold'] = (inputs['mdot_cold']/inputs['xs_area_cold']**2)/inputs['rho_cold'] * losses_cold - J['delta_p_cold', 'rho_cold'] = - dyn_press_cold / inputs['rho_cold'] * losses_cold - J['delta_p_cold', 'f_cold'] = dyn_press_cold * (-4*inputs['length_overall']/inputs['dh_cold']) - J['delta_p_cold', 'xs_area_cold'] = - 2 * dyn_press_cold / inputs['xs_area_cold'] * losses_cold - J['delta_p_cold', 'length_overall'] = dyn_press_cold * (-4 * inputs['f_cold'] / inputs['dh_cold']) - J['delta_p_cold', 'dh_cold'] = dyn_press_cold * (4*inputs['length_overall']*inputs['f_cold']/inputs['dh_cold']**2) - - J['delta_p_hot', 'mdot_hot'] = (inputs['mdot_hot']/inputs['xs_area_hot']**2)/inputs['rho_hot'] * losses_hot - J['delta_p_hot', 'rho_hot'] = - dyn_press_hot / inputs['rho_hot'] * losses_hot - J['delta_p_hot', 'f_hot'] = dyn_press_hot * (-4*inputs['width_overall']/inputs['dh_hot']) - J['delta_p_hot', 'xs_area_hot'] = - 2 * dyn_press_hot / inputs['xs_area_hot'] * losses_hot - J['delta_p_hot', 'width_overall'] = dyn_press_hot * (-4 * inputs['f_hot'] / inputs['dh_hot']) - J['delta_p_hot', 'dh_hot'] = dyn_press_hot * (4*inputs['width_overall']*inputs['f_hot']/inputs['dh_hot']**2) + dyn_press_cold = (1 / 2) * (inputs["mdot_cold"] / inputs["xs_area_cold"]) ** 2 / inputs["rho_cold"] + dyn_press_hot = (1 / 2) * (inputs["mdot_hot"] / inputs["xs_area_hot"]) ** 2 / inputs["rho_hot"] + Kec = self.options["Ke_cold"] + Kcc = self.options["Kc_cold"] + Keh = self.options["Ke_hot"] + Kch = self.options["Kc_hot"] + losses_cold = -Kec - Kcc - 4 * inputs["length_overall"] * inputs["f_cold"] / inputs["dh_cold"] + losses_hot = -Kec - Kcc - 4 * inputs["width_overall"] * inputs["f_hot"] / inputs["dh_hot"] + + J["delta_p_cold", "mdot_cold"] = ( + (inputs["mdot_cold"] / inputs["xs_area_cold"] ** 2) / inputs["rho_cold"] * losses_cold + ) + J["delta_p_cold", "rho_cold"] = -dyn_press_cold / inputs["rho_cold"] * losses_cold + J["delta_p_cold", "f_cold"] = dyn_press_cold * (-4 * inputs["length_overall"] / inputs["dh_cold"]) + J["delta_p_cold", "xs_area_cold"] = -2 * dyn_press_cold / inputs["xs_area_cold"] * losses_cold + J["delta_p_cold", "length_overall"] = dyn_press_cold * (-4 * inputs["f_cold"] / inputs["dh_cold"]) + J["delta_p_cold", "dh_cold"] = dyn_press_cold * ( + 4 * inputs["length_overall"] * inputs["f_cold"] / inputs["dh_cold"] ** 2 + ) + + J["delta_p_hot", "mdot_hot"] = ( + (inputs["mdot_hot"] / inputs["xs_area_hot"] ** 2) / inputs["rho_hot"] * losses_hot + ) + J["delta_p_hot", "rho_hot"] = -dyn_press_hot / inputs["rho_hot"] * losses_hot + J["delta_p_hot", "f_hot"] = dyn_press_hot * (-4 * inputs["width_overall"] / inputs["dh_hot"]) + J["delta_p_hot", "xs_area_hot"] = -2 * dyn_press_hot / inputs["xs_area_hot"] * losses_hot + J["delta_p_hot", "width_overall"] = dyn_press_hot * (-4 * inputs["f_hot"] / inputs["dh_hot"]) + J["delta_p_hot", "dh_hot"] = dyn_press_hot * ( + 4 * inputs["width_overall"] * inputs["f_hot"] / inputs["dh_hot"] ** 2 + ) + class HXGroup(Group): """ @@ -1205,60 +1387,70 @@ class HXGroup(Group): rho_hot : float Inflow density of the hot side (liquid) (vector, kg/m**3) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points' ) + self.options.declare("num_nodes", default=1, desc="Number of analysis points") def setup(self): - nn = self.options['num_nodes'] - - iv = self.add_subsystem('dv', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('case_thickness', val=2.0, units='mm') - iv.add_output('fin_thickness', val=0.102, units='mm') - iv.add_output('plate_thickness', val=0.2, units='mm') - iv.add_output('material_k', val=190, units='W/m/K') - iv.add_output('material_rho', val=2700, units='kg/m**3') - - #iv.add_output('mdot_cold', val=np.ones(nn)*1.5, units='kg/s') - #iv.add_output('rho_cold', val=np.ones(nn)*0.5, units='kg/m**3') - #iv.add_output('mdot_hot', val=0.075*np.ones(nn), units='kg/s') - #iv.add_output('rho_hot', val=np.ones(nn)*1020.2, units='kg/m**3') - - #iv.add_output('T_in_cold', val=np.ones(nn)*45, units='degC') - #iv.add_output('T_in_hot', val=np.ones(nn)*90, units='degC') + nn = self.options["num_nodes"] + + iv = self.add_subsystem("dv", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("case_thickness", val=2.0, units="mm") + iv.add_output("fin_thickness", val=0.102, units="mm") + iv.add_output("plate_thickness", val=0.2, units="mm") + iv.add_output("material_k", val=190, units="W/m/K") + iv.add_output("material_rho", val=2700, units="kg/m**3") + + # iv.add_output('mdot_cold', val=np.ones(nn)*1.5, units='kg/s') + # iv.add_output('rho_cold', val=np.ones(nn)*0.5, units='kg/m**3') + # iv.add_output('mdot_hot', val=0.075*np.ones(nn), units='kg/s') + # iv.add_output('rho_hot', val=np.ones(nn)*1020.2, units='kg/m**3') + + # iv.add_output('T_in_cold', val=np.ones(nn)*45, units='degC') + # iv.add_output('T_in_hot', val=np.ones(nn)*90, units='degC') # iv.add_output('n_long_cold', val=3) # iv.add_output('n_wide_cold', val=430) # iv.add_output('n_tall', val=19) - iv.add_output('channel_height_cold', val=14, units='mm') - iv.add_output('channel_width_cold', val=1.35, units='mm') - iv.add_output('fin_length_cold', val=6, units='mm') - iv.add_output('cp_cold', val=1005, units='J/kg/K') - iv.add_output('k_cold', val=0.02596, units='W/m/K') - iv.add_output('mu_cold', val=1.789e-5, units='kg/m/s') - - iv.add_output('channel_height_hot', val=1, units='mm') - iv.add_output('channel_width_hot', val=1, units='mm') - iv.add_output('fin_length_hot', val=6, units='mm') - iv.add_output('cp_hot', val=3801, units='J/kg/K') - iv.add_output('k_hot', val=0.405, units='W/m/K') - iv.add_output('mu_hot', val=1.68e-3, units='kg/m/s') - - dvlist = [['ac|propulsion|thermal|hx|n_wide_cold', 'n_wide_cold', 430, None], - ['ac|propulsion|thermal|hx|n_long_cold', 'n_long_cold', 3, None], - ['ac|propulsion|thermal|hx|n_tall', 'n_tall', 19, None]] - - self.add_subsystem('dvpassthru',DVLabel(dvlist),promotes_inputs=["*"],promotes_outputs=["*"]) - - - self.add_subsystem('osfgeometry', OffsetStripFinGeometry(), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('redh', HydraulicDiameterReynoldsNumber(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('osfdata', OffsetStripFinData(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('nusselt', NusseltFromColburnJ(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('convection', ConvectiveCoefficient(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('finefficiency', FinEfficiency(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('ua', UAOverall(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('ntu', NTUMethod(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('effectiveness', CrossFlowNTUEffectiveness(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('heat', NTUEffectivenessActualHeatTransfer(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('t_out', OutletTemperatures(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('delta_p', PressureDrop(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + iv.add_output("channel_height_cold", val=14, units="mm") + iv.add_output("channel_width_cold", val=1.35, units="mm") + iv.add_output("fin_length_cold", val=6, units="mm") + iv.add_output("cp_cold", val=1005, units="J/kg/K") + iv.add_output("k_cold", val=0.02596, units="W/m/K") + iv.add_output("mu_cold", val=1.789e-5, units="kg/m/s") + + iv.add_output("channel_height_hot", val=1, units="mm") + iv.add_output("channel_width_hot", val=1, units="mm") + iv.add_output("fin_length_hot", val=6, units="mm") + iv.add_output("cp_hot", val=3801, units="J/kg/K") + iv.add_output("k_hot", val=0.405, units="W/m/K") + iv.add_output("mu_hot", val=1.68e-3, units="kg/m/s") + + dvlist = [ + ["ac|propulsion|thermal|hx|n_wide_cold", "n_wide_cold", 430, None], + ["ac|propulsion|thermal|hx|n_long_cold", "n_long_cold", 3, None], + ["ac|propulsion|thermal|hx|n_tall", "n_tall", 19, None], + ] + + self.add_subsystem("dvpassthru", DVLabel(dvlist), promotes_inputs=["*"], promotes_outputs=["*"]) + + self.add_subsystem("osfgeometry", OffsetStripFinGeometry(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "redh", HydraulicDiameterReynoldsNumber(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("osfdata", OffsetStripFinData(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("nusselt", NusseltFromColburnJ(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "convection", ConvectiveCoefficient(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("finefficiency", FinEfficiency(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("ua", UAOverall(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("ntu", NTUMethod(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "effectiveness", CrossFlowNTUEffectiveness(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem( + "heat", NTUEffectivenessActualHeatTransfer(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("t_out", OutletTemperatures(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("delta_p", PressureDrop(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) diff --git a/openconcept/thermal/heat_pipe.py b/openconcept/thermal/heat_pipe.py index 0bb9333e..406a8350 100644 --- a/openconcept/thermal/heat_pipe.py +++ b/openconcept/thermal/heat_pipe.py @@ -4,6 +4,7 @@ from openconcept.utilities.constants import GRAV_CONST import warnings + class HeatPipe(Group): """ Model for an ammonia heat pipe. After model has been run, make sure that the @@ -24,7 +25,7 @@ class HeatPipe(Group): maintained for gradient optimization purposes (scalar, dimensionless) T_design : float Max temperature expected in heat pipe, used to compute weight (scalar, degC) - + Outputs ------- q_max : float @@ -33,7 +34,7 @@ class HeatPipe(Group): Temperature of connection to condenser end of heat pipe (vector, degC) weight : float Weight of heat pipe walls, excludes working fluid (scalar, kg) - + Options ------- num_nodes : int @@ -61,82 +62,129 @@ class HeatPipe(Group): q_max_warn : float User will be warned if q input exceeds q_max_warn * q_max, default 0.75 (scalar, dimensionless) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of design points to run') - self.options.declare('length_evap', default=0.25, desc='Length of evaporator m') - self.options.declare('length_cond', default=0.25, desc='Length of condenser m') - self.options.declare('wall_conduct', default=196., desc='Thermal conductivity of pipe wall material (default aluminum 7075) W/(m-K)') - self.options.declare('wick_thickness', default=0e-3, desc='Wick thickness in heat pipe m') - self.options.declare('wick_conduct', default=4., desc='Thermal conductivity of wick material and evaporation/condensation W/(m-K)') - self.options.declare('yield_stress', default=572., desc='Wall yield stress in MPa (default 7075)') - self.options.declare('rho_wall', default=2810., desc='Wall matl density in kg/m3 (default 7075)') - self.options.declare('stress_safety_factor', default=4., desc='FOS on the wall stress') - self.options.declare('theta', default=0., desc='Tilt from vertical deg') - self.options.declare('q_max_warn', default=.75, desc='Warning threshold for q exceeding q_max') - + self.options.declare("num_nodes", default=1, desc="Number of design points to run") + self.options.declare("length_evap", default=0.25, desc="Length of evaporator m") + self.options.declare("length_cond", default=0.25, desc="Length of condenser m") + self.options.declare( + "wall_conduct", + default=196.0, + desc="Thermal conductivity of pipe wall material (default aluminum 7075) W/(m-K)", + ) + self.options.declare("wick_thickness", default=0e-3, desc="Wick thickness in heat pipe m") + self.options.declare( + "wick_conduct", + default=4.0, + desc="Thermal conductivity of wick material and evaporation/condensation W/(m-K)", + ) + self.options.declare("yield_stress", default=572.0, desc="Wall yield stress in MPa (default 7075)") + self.options.declare("rho_wall", default=2810.0, desc="Wall matl density in kg/m3 (default 7075)") + self.options.declare("stress_safety_factor", default=4.0, desc="FOS on the wall stress") + self.options.declare("theta", default=0.0, desc="Tilt from vertical deg") + self.options.declare("q_max_warn", default=0.75, desc="Warning threshold for q exceeding q_max") + def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] # Scale the heat transfer by the number of pipes # Used ExecComp here because multiplying vector and scalar inputs - self.add_subsystem('heat_divide', ExecComp('q_div = q / n_pipes', - q_div={'units': 'W', 'shape': (nn,)}, - q={'units': 'W', 'shape': (nn,)}), - promotes_inputs=['q', 'n_pipes']) + self.add_subsystem( + "heat_divide", + ExecComp("q_div = q / n_pipes", q_div={"units": "W", "shape": (nn,)}, q={"units": "W", "shape": (nn,)}), + promotes_inputs=["q", "n_pipes"], + ) # Maximum heat transfer and weight - self.add_subsystem('q_max_calc', QMaxHeatPipe(num_nodes=nn, - theta=self.options['theta'], - yield_stress=self.options['yield_stress'], - rho_wall=self.options['rho_wall'], - stress_safety_factor=self.options['stress_safety_factor']), - # Assume temp in heat pipe is close to evaporator temp - promotes_inputs=['inner_diam', 'length', ('design_temp', 'T_design'), ('temp', 'T_evap')]) - + self.add_subsystem( + "q_max_calc", + QMaxHeatPipe( + num_nodes=nn, + theta=self.options["theta"], + yield_stress=self.options["yield_stress"], + rho_wall=self.options["rho_wall"], + stress_safety_factor=self.options["stress_safety_factor"], + ), + # Assume temp in heat pipe is close to evaporator temp + promotes_inputs=["inner_diam", "length", ("design_temp", "T_design"), ("temp", "T_evap")], + ) + # Multiply max heat transfer and weight by number of pipes multiply = ElementMultiplyDivideComp() - multiply.add_equation(output_name='weight', input_names=['single_pipe_weight', 'n_pipes'], input_units=['kg', None]) - self.add_subsystem('weight_multiplier', multiply, promotes_inputs=['n_pipes'], promotes_outputs=['weight']) - self.connect('q_max_calc.heat_pipe_weight', 'weight_multiplier.single_pipe_weight') + multiply.add_equation( + output_name="weight", input_names=["single_pipe_weight", "n_pipes"], input_units=["kg", None] + ) + self.add_subsystem("weight_multiplier", multiply, promotes_inputs=["n_pipes"], promotes_outputs=["weight"]) + self.connect("q_max_calc.heat_pipe_weight", "weight_multiplier.single_pipe_weight") # Used ExecComp here because multiplying vector and scalar inputs - self.add_subsystem('q_max_multiplier', ExecComp('q_max = single_pipe_q_max * n_pipes', - q_max={'units': 'W', 'shape': (nn,)}, - single_pipe_q_max={'units': 'W', 'shape': (nn,)}), - promotes_inputs=['n_pipes'], promotes_outputs=['q_max']) - self.connect('q_max_calc.q_max', 'q_max_multiplier.single_pipe_q_max') + self.add_subsystem( + "q_max_multiplier", + ExecComp( + "q_max = single_pipe_q_max * n_pipes", + q_max={"units": "W", "shape": (nn,)}, + single_pipe_q_max={"units": "W", "shape": (nn,)}, + ), + promotes_inputs=["n_pipes"], + promotes_outputs=["q_max"], + ) + self.connect("q_max_calc.q_max", "q_max_multiplier.single_pipe_q_max") # Thermal resistance at current operator condition - self.add_subsystem('ammonia_surrogate', AmmoniaProperties(num_nodes=nn), - # Assume temp in heat pipe is close to evaporator temp - promotes_inputs=[('temp', 'T_evap')]) - self.add_subsystem('delta_T_calc', HeatPipeVaporTempDrop(num_nodes=nn), - # Assume temp in heat pipe is close to evaporator temp - promotes_inputs=[('temp', 'T_evap'), 'inner_diam', 'length']) - self.add_subsystem('resistance', HeatPipeThermalResistance(num_nodes=nn, - length_evap=self.options['length_evap'], - length_cond=self.options['length_cond'], - wall_conduct=self.options['wall_conduct'], - wick_thickness=self.options['wick_thickness'], - wick_conduct=self.options['wick_conduct'], - vapor_resistance=True), - promotes_inputs=['inner_diam']) - self.connect('ammonia_surrogate.rho_vapor', 'delta_T_calc.rho_vapor') - self.connect('ammonia_surrogate.vapor_pressure', 'delta_T_calc.vapor_pressure') - self.connect('delta_T_calc.delta_T', 'resistance.delta_T') - self.connect('q_max_calc.wall_thickness', 'resistance.wall_thickness') # use wall thickness from hoop stress calc + self.add_subsystem( + "ammonia_surrogate", + AmmoniaProperties(num_nodes=nn), + # Assume temp in heat pipe is close to evaporator temp + promotes_inputs=[("temp", "T_evap")], + ) + self.add_subsystem( + "delta_T_calc", + HeatPipeVaporTempDrop(num_nodes=nn), + # Assume temp in heat pipe is close to evaporator temp + promotes_inputs=[("temp", "T_evap"), "inner_diam", "length"], + ) + self.add_subsystem( + "resistance", + HeatPipeThermalResistance( + num_nodes=nn, + length_evap=self.options["length_evap"], + length_cond=self.options["length_cond"], + wall_conduct=self.options["wall_conduct"], + wick_thickness=self.options["wick_thickness"], + wick_conduct=self.options["wick_conduct"], + vapor_resistance=True, + ), + promotes_inputs=["inner_diam"], + ) + self.connect("ammonia_surrogate.rho_vapor", "delta_T_calc.rho_vapor") + self.connect("ammonia_surrogate.vapor_pressure", "delta_T_calc.vapor_pressure") + self.connect("delta_T_calc.delta_T", "resistance.delta_T") + self.connect( + "q_max_calc.wall_thickness", "resistance.wall_thickness" + ) # use wall thickness from hoop stress calc # Compute condenser temperature - self.add_subsystem('cond_temp_calc', ExecComp('T_cond = T_evap - q*R', T_cond={'units': 'degC', 'shape': (nn,)}, - T_evap={'units': 'degC', 'shape': (nn,)}, - q={'units': 'W', 'shape': (nn,)}, - R={'units': 'K/W', 'shape': (nn,)}), - promotes_inputs=['T_evap'], promotes_outputs=['T_cond']) - self.connect('heat_divide.q_div', ['delta_T_calc.q', 'resistance.q', 'cond_temp_calc.q']) - self.connect('resistance.thermal_resistance', 'cond_temp_calc.R') + self.add_subsystem( + "cond_temp_calc", + ExecComp( + "T_cond = T_evap - q*R", + T_cond={"units": "degC", "shape": (nn,)}, + T_evap={"units": "degC", "shape": (nn,)}, + q={"units": "W", "shape": (nn,)}, + R={"units": "K/W", "shape": (nn,)}, + ), + promotes_inputs=["T_evap"], + promotes_outputs=["T_cond"], + ) + self.connect("heat_divide.q_div", ["delta_T_calc.q", "resistance.q", "cond_temp_calc.q"]) + self.connect("resistance.thermal_resistance", "cond_temp_calc.R") # Warn the user if heat transfer exceeds maximum possible - self.add_subsystem('q_max_warning', QMaxWarning(num_nodes=nn, q_max_warn=self.options['q_max_warn']), - promotes_inputs=['q', 'q_max']) + self.add_subsystem( + "q_max_warning", + QMaxWarning(num_nodes=nn, q_max_warn=self.options["q_max_warn"]), + promotes_inputs=["q", "q_max"], + ) + class HeatPipeThermalResistance(ExplicitComponent): """ @@ -181,51 +229,60 @@ class HeatPipeThermalResistance(ExplicitComponent): Set to true to include vapor resistance (usually negligible) in calculation, default false. If set to true, the q and temp inputs MUST be connected """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of design points to run') - self.options.declare('length_evap', default=0.25, desc='Length of evaporator m') - self.options.declare('length_cond', default=0.25, desc='Length of condenser m') - self.options.declare('wall_conduct', default=196., desc='Thermal conductivity of pipe wall material (default aluminum 7075) W/(m-K)') - self.options.declare('wick_thickness', default=0e-3, desc='Wick thickness in heat pipe m') - self.options.declare('wick_conduct', default=4., desc='Thermal conductivity of wick material and evaporation/condensation W/(m-K)') - self.options.declare('vapor_resistance', default=False, desc='Include vapor resistance in calculation') + self.options.declare("num_nodes", default=1, desc="Number of design points to run") + self.options.declare("length_evap", default=0.25, desc="Length of evaporator m") + self.options.declare("length_cond", default=0.25, desc="Length of condenser m") + self.options.declare( + "wall_conduct", + default=196.0, + desc="Thermal conductivity of pipe wall material (default aluminum 7075) W/(m-K)", + ) + self.options.declare("wick_thickness", default=0e-3, desc="Wick thickness in heat pipe m") + self.options.declare( + "wick_conduct", + default=4.0, + desc="Thermal conductivity of wick material and evaporation/condensation W/(m-K)", + ) + self.options.declare("vapor_resistance", default=False, desc="Include vapor resistance in calculation") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] arange = np.arange(0, nn) - self.add_input('inner_diam', units='m', val=0.02) - self.add_input('wall_thickness', val=1.25e-3, units='m') - self.add_input('q', shape=(nn,), units='W', val=800) - self.add_input('delta_T', shape=(nn,), units='K', val=0) + self.add_input("inner_diam", units="m", val=0.02) + self.add_input("wall_thickness", val=1.25e-3, units="m") + self.add_input("q", shape=(nn,), units="W", val=800) + self.add_input("delta_T", shape=(nn,), units="K", val=0) - self.add_output('thermal_resistance', shape=(nn,), units='K/W') + self.add_output("thermal_resistance", shape=(nn,), units="K/W") - self.declare_partials(['*'], ['*'], method='cs') + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): # Heat enters radially in through end of pipe (evaporator), along length of pipe, and radially out (condenser) # Resistance of evaporator - log_o_i_mesh = np.log(inputs['inner_diam']/(inputs['inner_diam'] - 2*self.options['wick_thickness'])) - R_mesh_evap = log_o_i_mesh / (2*np.pi * self.options['length_evap'] * self.options['wall_conduct']) - log_o_i_pipe = np.log((inputs['inner_diam'] + 2*inputs['wall_thickness'])/inputs['inner_diam']) - R_pipe_evap = log_o_i_pipe / (2*np.pi * self.options['length_evap'] * self.options['wall_conduct']) + log_o_i_mesh = np.log(inputs["inner_diam"] / (inputs["inner_diam"] - 2 * self.options["wick_thickness"])) + R_mesh_evap = log_o_i_mesh / (2 * np.pi * self.options["length_evap"] * self.options["wall_conduct"]) + log_o_i_pipe = np.log((inputs["inner_diam"] + 2 * inputs["wall_thickness"]) / inputs["inner_diam"]) + R_pipe_evap = log_o_i_pipe / (2 * np.pi * self.options["length_evap"] * self.options["wall_conduct"]) R_evap = R_mesh_evap + R_pipe_evap # Resistance of condenser - log_o_i_mesh = np.log(inputs['inner_diam']/(inputs['inner_diam'] - 2*self.options['wick_thickness'])) - R_mesh_cond = log_o_i_mesh / (2*np.pi * self.options['length_cond'] * self.options['wall_conduct']) - log_o_i_pipe = np.log((inputs['inner_diam'] + 2*inputs['wall_thickness'])/inputs['inner_diam']) - R_pipe_cond = log_o_i_pipe / (2*np.pi * self.options['length_cond'] * self.options['wall_conduct']) + log_o_i_mesh = np.log(inputs["inner_diam"] / (inputs["inner_diam"] - 2 * self.options["wick_thickness"])) + R_mesh_cond = log_o_i_mesh / (2 * np.pi * self.options["length_cond"] * self.options["wall_conduct"]) + log_o_i_pipe = np.log((inputs["inner_diam"] + 2 * inputs["wall_thickness"]) / inputs["inner_diam"]) + R_pipe_cond = log_o_i_pipe / (2 * np.pi * self.options["length_cond"] * self.options["wall_conduct"]) R_cond = R_mesh_cond + R_pipe_cond # Resistance of axial component in vapor R_vapor_axial = 0 - if self.options['vapor_resistance']: - R_vapor_axial = inputs['delta_T'] / inputs['q'] + if self.options["vapor_resistance"]: + R_vapor_axial = inputs["delta_T"] / inputs["q"] # Combine - outputs['thermal_resistance'] = R_evap + R_cond + R_vapor_axial + outputs["thermal_resistance"] = R_evap + R_cond + R_vapor_axial class HeatPipeVaporTempDrop(ExplicitComponent): @@ -255,7 +312,7 @@ class HeatPipeVaporTempDrop(ExplicitComponent): ------- delta_T : float Temperature differential across the vapor phase (vector, K) - + Options ------- num_nodes : int @@ -263,40 +320,48 @@ class HeatPipeVaporTempDrop(ExplicitComponent): Other options shouldn't be adjusted since they're for ammonia and there is a hardcoded curve fit also for ammonia in the compute method """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('latent_heat', default=1371.2e3, desc='Latent heat of vaporization J/kg') - self.options.declare('visc_base', default=9e-6, desc='Dynamic visc at 0C Pa s') - self.options.declare('visc_inc', default=4e-8, desc='Dynamic visc slope Pa s / deg') + self.options.declare("num_nodes", default=1) + self.options.declare("latent_heat", default=1371.2e3, desc="Latent heat of vaporization J/kg") + self.options.declare("visc_base", default=9e-6, desc="Dynamic visc at 0C Pa s") + self.options.declare("visc_inc", default=4e-8, desc="Dynamic visc slope Pa s / deg") def setup(self): - nn = self.options['num_nodes'] - self.add_input('q', val=3000*np.ones((nn,)), units='W') - self.add_input('temp', val=80*np.ones((nn,)), units='degC') - self.add_input('rho_vapor', val=np.ones((nn,)), units='kg/m**3') - self.add_input('vapor_pressure', val=np.ones((nn,))*1e5, units='Pa') - self.add_input('inner_diam', val=0.01, units='m') - self.add_input('length', val=6.6, units='m') - self.add_output('delta_T', val=0.1*np.ones((nn,)), units='K') - self.declare_partials(['*'], ['*'], method='cs') + nn = self.options["num_nodes"] + self.add_input("q", val=3000 * np.ones((nn,)), units="W") + self.add_input("temp", val=80 * np.ones((nn,)), units="degC") + self.add_input("rho_vapor", val=np.ones((nn,)), units="kg/m**3") + self.add_input("vapor_pressure", val=np.ones((nn,)) * 1e5, units="Pa") + self.add_input("inner_diam", val=0.01, units="m") + self.add_input("length", val=6.6, units="m") + self.add_output("delta_T", val=0.1 * np.ones((nn,)), units="K") + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): # If the input temperature is out of the range of the fitting data, raise a warning - if np.any(inputs['temp'] < -10) or np.any(inputs['temp'] > 100): - warnings.warn(self.msginfo + f" Heat pipe input temperature of {np.max(inputs['temp'])} deg C is outside of the -10 to 100 deg C " - "temperature range of the data used for the temp-pressure curve fit. Results may be invalid.", stacklevel=2) - - mdot = inputs['q'] / self.options['latent_heat'] - area = np.pi * inputs['inner_diam'] ** 2 / 4 - mean_vel = mdot / inputs['rho_vapor'] / area - visc = inputs['temp'] * self.options['visc_inc'] + self.options['visc_base'] - redh = inputs['rho_vapor'] * mean_vel * inputs['inner_diam'] / visc - darcy_f = 0.3164 * redh ** (-1/4) # blasius correlation for smooth pipes - delta_p = inputs['length'] * darcy_f * inputs['rho_vapor'] * mean_vel ** 2 / 2 / inputs['inner_diam'] - pressure_kPa = inputs['vapor_pressure'] / 1000 - - dt_dp = (35.976 / pressure_kPa) / 1000 # based on log curve fit of T vs P curve of ammonia vapor from -10 to 100 C - outputs['delta_T'] = dt_dp * delta_p + if np.any(inputs["temp"] < -10) or np.any(inputs["temp"] > 100): + warnings.warn( + self.msginfo + + f" Heat pipe input temperature of {np.max(inputs['temp'])} deg C is outside of the -10 to 100 deg C " + "temperature range of the data used for the temp-pressure curve fit. Results may be invalid.", + stacklevel=2, + ) + + mdot = inputs["q"] / self.options["latent_heat"] + area = np.pi * inputs["inner_diam"] ** 2 / 4 + mean_vel = mdot / inputs["rho_vapor"] / area + visc = inputs["temp"] * self.options["visc_inc"] + self.options["visc_base"] + redh = inputs["rho_vapor"] * mean_vel * inputs["inner_diam"] / visc + darcy_f = 0.3164 * redh ** (-1 / 4) # blasius correlation for smooth pipes + delta_p = inputs["length"] * darcy_f * inputs["rho_vapor"] * mean_vel**2 / 2 / inputs["inner_diam"] + pressure_kPa = inputs["vapor_pressure"] / 1000 + + dt_dp = ( + 35.976 / pressure_kPa + ) / 1000 # based on log curve fit of T vs P curve of ammonia vapor from -10 to 100 C + outputs["delta_T"] = dt_dp * delta_p + class HeatPipeWeight(ExplicitComponent): """ @@ -328,39 +393,48 @@ class HeatPipeWeight(ExplicitComponent): stress_safety_factor : float Factor of safety for the wall hoop stress """ + def initialize(self): - self.options.declare('yield_stress', default=572., desc='Wall yield stress in MPa (default 7075)') - self.options.declare('rho_wall', default=2810., desc='Wall matl density in kg/m3 (default 7075)') - self.options.declare('stress_safety_factor', default=4., desc='FOS on the wall stress') + self.options.declare("yield_stress", default=572.0, desc="Wall yield stress in MPa (default 7075)") + self.options.declare("rho_wall", default=2810.0, desc="Wall matl density in kg/m3 (default 7075)") + self.options.declare("stress_safety_factor", default=4.0, desc="FOS on the wall stress") def setup(self): - self.add_input('design_pressure', units='MPa', val=1.) - self.add_input('inner_diam', units='m', val=0.01) - self.add_input('length', units='m', val=6.6) - self.add_output('heat_pipe_weight', units='kg') - self.add_output('wall_thickness', units='m') + self.add_input("design_pressure", units="MPa", val=1.0) + self.add_input("inner_diam", units="m", val=0.01) + self.add_input("length", units="m", val=6.6) + self.add_output("heat_pipe_weight", units="kg") + self.add_output("wall_thickness", units="m") # it's just as fast to CS this simple comp - self.declare_partials(['heat_pipe_weight'], ['length', 'design_pressure', 'inner_diam']) - self.declare_partials(['wall_thickness'], ['design_pressure', 'inner_diam']) - + self.declare_partials(["heat_pipe_weight"], ["length", "design_pressure", "inner_diam"]) + self.declare_partials(["wall_thickness"], ["design_pressure", "inner_diam"]) def compute(self, inputs, outputs): - sigma_y = self.options['yield_stress'] - FOS = self.options['stress_safety_factor'] - rho = self.options['rho_wall'] - outputs['wall_thickness'] = inputs['design_pressure'] * inputs['inner_diam'] * FOS / sigma_y - outputs['heat_pipe_weight'] = inputs['length'] * np.pi * inputs['inner_diam'] ** 2 * inputs['design_pressure'] * FOS * rho / sigma_y + sigma_y = self.options["yield_stress"] + FOS = self.options["stress_safety_factor"] + rho = self.options["rho_wall"] + outputs["wall_thickness"] = inputs["design_pressure"] * inputs["inner_diam"] * FOS / sigma_y + outputs["heat_pipe_weight"] = ( + inputs["length"] * np.pi * inputs["inner_diam"] ** 2 * inputs["design_pressure"] * FOS * rho / sigma_y + ) def compute_partials(self, inputs, J): - sigma_y = self.options['yield_stress'] - FOS = self.options['stress_safety_factor'] - rho = self.options['rho_wall'] - J['wall_thickness', 'design_pressure'] = inputs['inner_diam'] * FOS / sigma_y - J['wall_thickness', 'inner_diam'] = inputs['design_pressure'] * FOS / sigma_y - J['heat_pipe_weight', 'design_pressure'] = inputs['length'] * np.pi * inputs['inner_diam'] ** 2 * FOS * rho / sigma_y - J['heat_pipe_weight', 'inner_diam'] = inputs['length'] * np.pi * 2 * inputs['inner_diam'] * inputs['design_pressure'] * FOS * rho / sigma_y - J['heat_pipe_weight', 'length'] = np.pi * inputs['inner_diam'] ** 2 * inputs['design_pressure'] * FOS * rho / sigma_y + sigma_y = self.options["yield_stress"] + FOS = self.options["stress_safety_factor"] + rho = self.options["rho_wall"] + J["wall_thickness", "design_pressure"] = inputs["inner_diam"] * FOS / sigma_y + J["wall_thickness", "inner_diam"] = inputs["design_pressure"] * FOS / sigma_y + J["heat_pipe_weight", "design_pressure"] = ( + inputs["length"] * np.pi * inputs["inner_diam"] ** 2 * FOS * rho / sigma_y + ) + J["heat_pipe_weight", "inner_diam"] = ( + inputs["length"] * np.pi * 2 * inputs["inner_diam"] * inputs["design_pressure"] * FOS * rho / sigma_y + ) + J["heat_pipe_weight", "length"] = ( + np.pi * inputs["inner_diam"] ** 2 * inputs["design_pressure"] * FOS * rho / sigma_y + ) + class AmmoniaProperties(Group): """ @@ -373,7 +447,7 @@ class AmmoniaProperties(Group): ------ temp : float Temperature of ammonia liquid/vapor (vector, degC) - + Outputs ------- rho_liquid : float @@ -382,39 +456,169 @@ class AmmoniaProperties(Group): Ammonia vapor density (vector, kg/m^3) vapor_pressure : float Ammonia vapor pressure (vector, kPa) - + Options ------- num_nodes : int Number of analysis points to run, default 1 (scalar, dimensionless) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] # Surrogate model data from https://en.wikipedia.org/wiki/Ammonia_(data_page)#Vapor%E2%80%93liquid_equilibrium_data - temp_avg = np.array([-75,-70,-65,-60,-55,-50,-45,-40,-35,-30,-25,-20,-15, - -10,-5,0,5,10,15,20,25,30,35,40,45,50,60,70,80,90,100]) # deg C - rho_liquid = np.array([730.94,725.27,719.53,713.78,707.91,702,696.04,689.99,683.85,677.64, - 671.37,665.03,658.54,651.98,645.33,638.57,631.67,624.69,617.55,610.28, - 602.85,595.24,588.16,579.48,571.3,562.87,545.23,526.32,505.71,482.9,456.93]) # kg/m^3 - rho_vapor = np.array([0.078241,0.11141,0.15552,0.21321,0.28596,0.38158,0.4994,0.64508,0.82318, - 1.0386,1.2969,1.6039,1.9659,2.3874,2.8827,3.4528,4.1086,4.8593,5.7153,6.6876, - 7.7882,9.031,10.431,12.006,13.775,15.761,20.5,26.5,34.1,43.9,56.8]) # kg/m^3 - vapor_pressure = np.array([7.93, 10.92, 15.61, 21.90, 30.16, 40.87, 54.54, 71.77, 93.19, - 119.6, 151.6, 190.2, 236.3, 290.8, 354.8, 429.4, 515.7, 614.9, - 728.3, 857.1, 1003., 1166., 1350., 1554., 1781., 2032., - 2613., 3312., 4144., 5123., 6264.]) # kPa + temp_avg = np.array( + [ + -75, + -70, + -65, + -60, + -55, + -50, + -45, + -40, + -35, + -30, + -25, + -20, + -15, + -10, + -5, + 0, + 5, + 10, + 15, + 20, + 25, + 30, + 35, + 40, + 45, + 50, + 60, + 70, + 80, + 90, + 100, + ] + ) # deg C + rho_liquid = np.array( + [ + 730.94, + 725.27, + 719.53, + 713.78, + 707.91, + 702, + 696.04, + 689.99, + 683.85, + 677.64, + 671.37, + 665.03, + 658.54, + 651.98, + 645.33, + 638.57, + 631.67, + 624.69, + 617.55, + 610.28, + 602.85, + 595.24, + 588.16, + 579.48, + 571.3, + 562.87, + 545.23, + 526.32, + 505.71, + 482.9, + 456.93, + ] + ) # kg/m^3 + rho_vapor = np.array( + [ + 0.078241, + 0.11141, + 0.15552, + 0.21321, + 0.28596, + 0.38158, + 0.4994, + 0.64508, + 0.82318, + 1.0386, + 1.2969, + 1.6039, + 1.9659, + 2.3874, + 2.8827, + 3.4528, + 4.1086, + 4.8593, + 5.7153, + 6.6876, + 7.7882, + 9.031, + 10.431, + 12.006, + 13.775, + 15.761, + 20.5, + 26.5, + 34.1, + 43.9, + 56.8, + ] + ) # kg/m^3 + vapor_pressure = np.array( + [ + 7.93, + 10.92, + 15.61, + 21.90, + 30.16, + 40.87, + 54.54, + 71.77, + 93.19, + 119.6, + 151.6, + 190.2, + 236.3, + 290.8, + 354.8, + 429.4, + 515.7, + 614.9, + 728.3, + 857.1, + 1003.0, + 1166.0, + 1350.0, + 1554.0, + 1781.0, + 2032.0, + 2613.0, + 3312.0, + 4144.0, + 5123.0, + 6264.0, + ] + ) # kPa # Set up the surrogate model to determine densities - interp = MetaModelStructuredComp(method='cubic', extrapolate=True, vec_size=nn) - interp.add_input('temp', val=40., shape=(nn,), units='degC', training_data=temp_avg) - interp.add_output('rho_liquid', val=579.48, shape=(nn,), units='kg/m**3', training_data=rho_liquid) - interp.add_output('rho_vapor', val=12.006, shape=(nn,), units='kg/m**3', training_data=rho_vapor) - interp.add_output('vapor_pressure', val=6000, shape=(nn,), units='kPa', training_data=vapor_pressure) - self.add_subsystem('surrogate', interp, promotes=['*']) + interp = MetaModelStructuredComp(method="cubic", extrapolate=True, vec_size=nn) + interp.add_input("temp", val=40.0, shape=(nn,), units="degC", training_data=temp_avg) + interp.add_output("rho_liquid", val=579.48, shape=(nn,), units="kg/m**3", training_data=rho_liquid) + interp.add_output("rho_vapor", val=12.006, shape=(nn,), units="kg/m**3", training_data=rho_vapor) + interp.add_output("vapor_pressure", val=6000, shape=(nn,), units="kPa", training_data=vapor_pressure) + self.add_subsystem("surrogate", interp, promotes=["*"]) + class QMaxHeatPipe(Group): """ @@ -456,37 +660,53 @@ class QMaxHeatPipe(Group): stress_safety_factor : float Factor of safety for the wall hoop stress """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - self.options.declare('theta', default=0., desc='Tilt from vertical degrees') - self.options.declare('yield_stress', default=572., desc='Wall yield stress in MPa (default 7075)') - self.options.declare('rho_wall', default=2810., desc='Wall matl density in kg/m3 (default 7075)') - self.options.declare('stress_safety_factor', default=4., desc='FOS on the wall stress') + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + self.options.declare("theta", default=0.0, desc="Tilt from vertical degrees") + self.options.declare("yield_stress", default=572.0, desc="Wall yield stress in MPa (default 7075)") + self.options.declare("rho_wall", default=2810.0, desc="Wall matl density in kg/m3 (default 7075)") + self.options.declare("stress_safety_factor", default=4.0, desc="FOS on the wall stress") def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] # Ammonia properties from surrogate model - self.add_subsystem('ammonia_current', AmmoniaProperties(num_nodes=nn), - promotes_inputs=['temp'], promotes_outputs=['vapor_pressure']) - self.add_subsystem('ammonia_design', AmmoniaProperties(), - promotes_inputs=[('temp', 'design_temp')], - promotes_outputs=[('vapor_pressure', 'design_pressure')]) + self.add_subsystem( + "ammonia_current", + AmmoniaProperties(num_nodes=nn), + promotes_inputs=["temp"], + promotes_outputs=["vapor_pressure"], + ) + self.add_subsystem( + "ammonia_design", + AmmoniaProperties(), + promotes_inputs=[("temp", "design_temp")], + promotes_outputs=[("vapor_pressure", "design_pressure")], + ) # Take in the densities from the surrogate model and use analytical expressions to get Q max - self.add_subsystem('q_max_calc', - QMaxAnalyticalPart(num_nodes=nn, theta=self.options['theta']), - promotes_inputs=['inner_diam', 'temp'], - promotes_outputs=['q_max']) - - self.add_subsystem('weight_calc', HeatPipeWeight(yield_stress=self.options['yield_stress'], - rho_wall=self.options['rho_wall'], - stress_safety_factor=self.options['stress_safety_factor']), - promotes=['*']) + self.add_subsystem( + "q_max_calc", + QMaxAnalyticalPart(num_nodes=nn, theta=self.options["theta"]), + promotes_inputs=["inner_diam", "temp"], + promotes_outputs=["q_max"], + ) + + self.add_subsystem( + "weight_calc", + HeatPipeWeight( + yield_stress=self.options["yield_stress"], + rho_wall=self.options["rho_wall"], + stress_safety_factor=self.options["stress_safety_factor"], + ), + promotes=["*"], + ) # Connect surrogate to analytical expressions - self.connect('ammonia_current.rho_liquid', 'q_max_calc.rho_liquid') - self.connect('ammonia_current.rho_vapor', 'q_max_calc.rho_vapor') + self.connect("ammonia_current.rho_liquid", "q_max_calc.rho_liquid") + self.connect("ammonia_current.rho_vapor", "q_max_calc.rho_vapor") + class QMaxAnalyticalPart(ExplicitComponent): """ @@ -527,41 +747,49 @@ class QMaxAnalyticalPart(ExplicitComponent): Surface tension sensitivity w.r.t. temperature (used for linear estimate), default ammonia (in 0-50 deg C range) -2.3e-4 N/m/degC (scalar, N/m/degC) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - self.options.declare('theta', default=0., desc='Tilt from vertical degrees') - self.options.declare('latent_heat', default=1371.2e3, desc='Latent heat of vaporization J/kg') - self.options.declare('surface_tension_base', default=0.026, desc='Surface tension at 0 deg C N/m') - self.options.declare('surface_tension_incr', default=-2.3e-4, desc='Surface tension derivative w.r.t. temp N/m/degC') + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + self.options.declare("theta", default=0.0, desc="Tilt from vertical degrees") + self.options.declare("latent_heat", default=1371.2e3, desc="Latent heat of vaporization J/kg") + self.options.declare("surface_tension_base", default=0.026, desc="Surface tension at 0 deg C N/m") + self.options.declare( + "surface_tension_incr", default=-2.3e-4, desc="Surface tension derivative w.r.t. temp N/m/degC" + ) def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] - self.add_input('inner_diam', units='m', val=0.01) - self.add_input('temp', units='degC', val=40., shape=(nn,)) - self.add_input('rho_liquid', val=579.48, shape=(nn,), units='kg/m**3') - self.add_input('rho_vapor', val=12.006, shape=(nn,), units='kg/m**3') + self.add_input("inner_diam", units="m", val=0.01) + self.add_input("temp", units="degC", val=40.0, shape=(nn,)) + self.add_input("rho_liquid", val=579.48, shape=(nn,), units="kg/m**3") + self.add_input("rho_vapor", val=12.006, shape=(nn,), units="kg/m**3") - self.add_output('q_max', shape=(nn,), units='W') + self.add_output("q_max", shape=(nn,), units="W") - self.declare_partials(['*'], ['*'], method='cs') + self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - rho_L = inputs['rho_liquid'] - rho_V = inputs['rho_vapor'] - A_vapor = np.pi/4 * inputs['inner_diam']**2 # heat pipe cross sectional area - latent = self.options['latent_heat'] - th_rad = self.options['theta'] * np.pi / 180 # rad + rho_L = inputs["rho_liquid"] + rho_V = inputs["rho_vapor"] + A_vapor = np.pi / 4 * inputs["inner_diam"] ** 2 # heat pipe cross sectional area + latent = self.options["latent_heat"] + th_rad = self.options["theta"] * np.pi / 180 # rad # Use linear estimate for surface tension, see p. 16 of http://web.iiar.org/membersonly/PDF/CO/databook_ch2.pdf (accessed Aug 9 2022) - surface_tension = self.options['surface_tension_base'] + self.options['surface_tension_incr'] * inputs['temp'] + surface_tension = self.options["surface_tension_base"] + self.options["surface_tension_incr"] * inputs["temp"] # Compute Q max using equations from https://www.1-act.com/resources/heat-pipe-performance/ (accessed Aug 9 2022) - bond_number = inputs['inner_diam'] * np.sqrt(GRAV_CONST/surface_tension * (rho_L - rho_V)) - k_flooding = (rho_L/rho_V)**0.14 * np.tanh(bond_number**0.25)**2 - q_max_numer = k_flooding * A_vapor * latent * (GRAV_CONST * np.sin(np.pi/2 - th_rad) * surface_tension * (rho_L - rho_V))**0.25 - q_max_denom = (rho_L**-0.25 + rho_V**-0.25)**2 - outputs['q_max'] = q_max_numer / q_max_denom + bond_number = inputs["inner_diam"] * np.sqrt(GRAV_CONST / surface_tension * (rho_L - rho_V)) + k_flooding = (rho_L / rho_V) ** 0.14 * np.tanh(bond_number**0.25) ** 2 + q_max_numer = ( + k_flooding + * A_vapor + * latent + * (GRAV_CONST * np.sin(np.pi / 2 - th_rad) * surface_tension * (rho_L - rho_V)) ** 0.25 + ) + q_max_denom = (rho_L**-0.25 + rho_V**-0.25) ** 2 + outputs["q_max"] = q_max_numer / q_max_denom class QMaxWarning(ExplicitComponent): @@ -574,11 +802,11 @@ class QMaxWarning(ExplicitComponent): Heat transferred from evaporator side to condenser side by heat pipe (vector, W) q_max : float Maximum heat transfer possible by heat pipes before dry-out (vector, W) - + Outputs ------- None - + Options ------- num_nodes : int @@ -586,20 +814,24 @@ class QMaxWarning(ExplicitComponent): q_max_warn : float User will be warned if q input exceeds q_max_warn * q_max, default 0.75 (scalar, dimensionless) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of design points to run') - self.options.declare('q_max_warn', default=.75, desc='Warning threshold for q exceeding q_max') - + self.options.declare("num_nodes", default=1, desc="Number of design points to run") + self.options.declare("q_max_warn", default=0.75, desc="Warning threshold for q exceeding q_max") + def setup(self): - nn = self.options['num_nodes'] - self.add_input('q', shape=(nn,), val=0., units='W') - self.add_input('q_max', shape=(nn,), units='W') - + nn = self.options["num_nodes"] + self.add_input("q", shape=(nn,), val=0.0, units="W") + self.add_input("q_max", shape=(nn,), units="W") + def compute(self, inputs, outputs): - q = inputs['q'] - q_max = inputs['q_max'] - q_warn = self.options['q_max_warn'] + q = inputs["q"] + q_max = inputs["q_max"] + q_warn = self.options["q_max_warn"] if np.any(q > q_warn * q_max): - warnings.warn(self.msginfo + f" Heat pipe is being asked to transfer " - f"{np.max(q/q_max)*100:2.1f}% of its maximum heat transfer capability. This is {(np.max(q/q_max) - q_warn)*100:2.1f}% over " - f"the warning threshold of {q_warn*100:2.1f}%.", stacklevel=2) + warnings.warn( + self.msginfo + f" Heat pipe is being asked to transfer " + f"{np.max(q/q_max)*100:2.1f}% of its maximum heat transfer capability. This is {(np.max(q/q_max) - q_warn)*100:2.1f}% over " + f"the warning threshold of {q_warn*100:2.1f}%.", + stacklevel=2, + ) diff --git a/openconcept/thermal/hose.py b/openconcept/thermal/hose.py index a91dfe40..1691b04a 100644 --- a/openconcept/thermal/hose.py +++ b/openconcept/thermal/hose.py @@ -1,5 +1,6 @@ -import openmdao.api as om -import numpy as np +import openmdao.api as om +import numpy as np + class SimpleHose(om.ExplicitComponent): """ @@ -36,87 +37,93 @@ class SimpleHose(om.ExplicitComponent): hose_density : float Material density of the hose (kg/m3) set to 0.049 lb/in3 equivalent per empirical data """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('hose_operating_stress', default=2.07e6, desc='Hoop stress at max op press in Pa') - self.options.declare('hose_density', default=1356.3, desc='Hose matl density in kg/m3') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("hose_operating_stress", default=2.07e6, desc="Hoop stress at max op press in Pa") + self.options.declare("hose_density", default=1356.3, desc="Hose matl density in kg/m3") def setup(self): - nn = self.options['num_nodes'] - self.add_input('hose_diameter', val=0.0254, units='m') - self.add_input('hose_length', val=1.0, units='m') - self.add_input('hose_design_pressure', units='Pa', val=1.03e6, desc='Hose max operating pressure') - - - self.add_input('mdot_coolant', units='kg/s', desc='Coolant mass flow rate', val=np.ones((nn,))) - self.add_input('rho_coolant', units='kg/m**3', desc='Coolant density', val=1020.*np.ones((nn,))) - self.add_input('mu_coolant', val=1.68e-3, units='kg/m/s', desc='Coolant viscosity') - - self.add_output('delta_p', units='Pa', desc='Hose pressure drop', val=np.ones((nn,))) - self.add_output('component_weight', units='kg', desc='Pump weight') - - self.declare_partials(['delta_p'], ['rho_coolant', 'mdot_coolant'], rows=np.arange(nn), cols=np.arange(nn)) - self.declare_partials(['delta_p'], ['hose_diameter', 'hose_length', 'mu_coolant'], rows=np.arange(nn), cols=np.zeros(nn)) - self.declare_partials(['component_weight'], ['hose_design_pressure','hose_length','hose_diameter'], rows=[0], cols=[0]) - self.declare_partials(['component_weight'], ['rho_coolant'], rows=[0], cols=[0]) - + nn = self.options["num_nodes"] + self.add_input("hose_diameter", val=0.0254, units="m") + self.add_input("hose_length", val=1.0, units="m") + self.add_input("hose_design_pressure", units="Pa", val=1.03e6, desc="Hose max operating pressure") + + self.add_input("mdot_coolant", units="kg/s", desc="Coolant mass flow rate", val=np.ones((nn,))) + self.add_input("rho_coolant", units="kg/m**3", desc="Coolant density", val=1020.0 * np.ones((nn,))) + self.add_input("mu_coolant", val=1.68e-3, units="kg/m/s", desc="Coolant viscosity") + + self.add_output("delta_p", units="Pa", desc="Hose pressure drop", val=np.ones((nn,))) + self.add_output("component_weight", units="kg", desc="Pump weight") + + self.declare_partials(["delta_p"], ["rho_coolant", "mdot_coolant"], rows=np.arange(nn), cols=np.arange(nn)) + self.declare_partials( + ["delta_p"], ["hose_diameter", "hose_length", "mu_coolant"], rows=np.arange(nn), cols=np.zeros(nn) + ) + self.declare_partials( + ["component_weight"], ["hose_design_pressure", "hose_length", "hose_diameter"], rows=[0], cols=[0] + ) + self.declare_partials(["component_weight"], ["rho_coolant"], rows=[0], cols=[0]) def _compute_pressure_drop(self, inputs): - xs_area = np.pi * (inputs['hose_diameter'] / 2) ** 2 - U = inputs['mdot_coolant'] / inputs['rho_coolant'] / xs_area - Redh = inputs['rho_coolant'] * U * inputs['hose_diameter'] / inputs['mu_coolant'] + xs_area = np.pi * (inputs["hose_diameter"] / 2) ** 2 + U = inputs["mdot_coolant"] / inputs["rho_coolant"] / xs_area + Redh = inputs["rho_coolant"] * U * inputs["hose_diameter"] / inputs["mu_coolant"] # darcy friction from the Blasius correlation - f = 0.3164 * Redh ** (-1/4) - dp = f * inputs['rho_coolant'] * U ** 2 * inputs['hose_length'] / 2 / inputs['hose_diameter'] + f = 0.3164 * Redh ** (-1 / 4) + dp = f * inputs["rho_coolant"] * U**2 * inputs["hose_length"] / 2 / inputs["hose_diameter"] return dp def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - sigma = self.options['hose_operating_stress'] - rho_hose = self.options['hose_density'] + nn = self.options["num_nodes"] + sigma = self.options["hose_operating_stress"] + rho_hose = self.options["hose_density"] - outputs['delta_p'] = self._compute_pressure_drop(inputs) + outputs["delta_p"] = self._compute_pressure_drop(inputs) - thickness = inputs['hose_diameter'] * inputs['hose_design_pressure'] / 2 / sigma - - w_hose = (inputs['hose_diameter'] + thickness) * np.pi * thickness * rho_hose * inputs['hose_length'] - w_coolant = (inputs['hose_diameter'] / 2) ** 2 * np.pi * inputs['rho_coolant'][0] * inputs['hose_length'] - outputs['component_weight'] = w_hose + w_coolant - + thickness = inputs["hose_diameter"] * inputs["hose_design_pressure"] / 2 / sigma + + w_hose = (inputs["hose_diameter"] + thickness) * np.pi * thickness * rho_hose * inputs["hose_length"] + w_coolant = (inputs["hose_diameter"] / 2) ** 2 * np.pi * inputs["rho_coolant"][0] * inputs["hose_length"] + outputs["component_weight"] = w_hose + w_coolant def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - sigma = self.options['hose_operating_stress'] - rho_hose = self.options['hose_density'] - thickness = inputs['hose_diameter'] * inputs['hose_design_pressure'] / 2 / sigma - - d_thick_d_diam = inputs['hose_design_pressure'] / 2 / sigma - d_thick_d_press = inputs['hose_diameter'] / 2 / sigma - - J['component_weight','rho_coolant'] = (inputs['hose_diameter'] / 2) ** 2 * np.pi * inputs['hose_length'] - J['component_weight', 'hose_design_pressure'] = (inputs['hose_diameter'] + thickness) * np.pi * d_thick_d_press * \ - rho_hose * inputs['hose_length'] + np.pi * thickness * rho_hose * \ - inputs['hose_length'] * d_thick_d_press - J['component_weight', 'hose_length'] = (inputs['hose_diameter'] + thickness) * np.pi * thickness * rho_hose + \ - (inputs['hose_diameter'] / 2) ** 2 * np.pi * inputs['rho_coolant'][0] - J['component_weight', 'hose_diameter'] = (inputs['hose_diameter'] + thickness) * np.pi * d_thick_d_diam * rho_hose * \ - inputs['hose_length'] + (1 + d_thick_d_diam) * np.pi * thickness * rho_hose * \ - inputs['hose_length'] + inputs['hose_diameter'] / 2 * np.pi * \ - inputs['rho_coolant'][0] * inputs['hose_length'] + nn = self.options["num_nodes"] + sigma = self.options["hose_operating_stress"] + rho_hose = self.options["hose_density"] + thickness = inputs["hose_diameter"] * inputs["hose_design_pressure"] / 2 / sigma + + d_thick_d_diam = inputs["hose_design_pressure"] / 2 / sigma + d_thick_d_press = inputs["hose_diameter"] / 2 / sigma + + J["component_weight", "rho_coolant"] = (inputs["hose_diameter"] / 2) ** 2 * np.pi * inputs["hose_length"] + J["component_weight", "hose_design_pressure"] = ( + inputs["hose_diameter"] + thickness + ) * np.pi * d_thick_d_press * rho_hose * inputs["hose_length"] + np.pi * thickness * rho_hose * inputs[ + "hose_length" + ] * d_thick_d_press + J["component_weight", "hose_length"] = (inputs["hose_diameter"] + thickness) * np.pi * thickness * rho_hose + ( + inputs["hose_diameter"] / 2 + ) ** 2 * np.pi * inputs["rho_coolant"][0] + J["component_weight", "hose_diameter"] = ( + (inputs["hose_diameter"] + thickness) * np.pi * d_thick_d_diam * rho_hose * inputs["hose_length"] + + (1 + d_thick_d_diam) * np.pi * thickness * rho_hose * inputs["hose_length"] + + inputs["hose_diameter"] / 2 * np.pi * inputs["rho_coolant"][0] * inputs["hose_length"] + ) # use a colored complex step approach cs_step = 1e-30 dp_base = self._compute_pressure_drop(inputs) - cs_inp_list = ['rho_coolant', 'mdot_coolant', 'hose_diameter', 'hose_length', 'mu_coolant'] + cs_inp_list = ["rho_coolant", "mdot_coolant", "hose_diameter", "hose_length", "mu_coolant"] fake_inputs = dict() # make a perturbable, complex copy of the inputs for inp in cs_inp_list: fake_inputs[inp] = inputs[inp].astype(np.complex_, copy=True) - + for inp in cs_inp_list: arr_to_restore = fake_inputs[inp].copy() - fake_inputs[inp] += (0.0+cs_step*1.0j) + fake_inputs[inp] += 0.0 + cs_step * 1.0j dp_perturbed = self._compute_pressure_drop(fake_inputs) fake_inputs[inp] = arr_to_restore - J['delta_p', inp] = np.imag(dp_perturbed) / cs_step + J["delta_p", inp] = np.imag(dp_perturbed) / cs_step diff --git a/openconcept/thermal/manifold.py b/openconcept/thermal/manifold.py index a3a84312..0c8989ad 100644 --- a/openconcept/thermal/manifold.py +++ b/openconcept/thermal/manifold.py @@ -13,47 +13,50 @@ class FlowSplit(ExplicitComponent): mdot_split_fraction : float Fraction of incoming mass flow directed to output A, must be in range 0-1 inclusive (vector, dimensionless) - + Outputs ------- mdot_out_A : float Mass flow rate directed to first output (vector, kg/s) mdot_out_B : float Mass flow rate directed to second output (vector, kg/s) - + Options ------- num_nodes : int Number of analysis points to run (sets vec length; default 1) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] rng = np.arange(0, nn) - self.add_input('mdot_in', units='kg/s', shape=(nn,)) - self.add_input('mdot_split_fraction', units=None, shape=(nn,), val=0.5) + self.add_input("mdot_in", units="kg/s", shape=(nn,)) + self.add_input("mdot_split_fraction", units=None, shape=(nn,), val=0.5) - self.add_output('mdot_out_A', units='kg/s', shape=(nn,)) - self.add_output('mdot_out_B', units='kg/s', shape=(nn,)) + self.add_output("mdot_out_A", units="kg/s", shape=(nn,)) + self.add_output("mdot_out_B", units="kg/s", shape=(nn,)) + + self.declare_partials(["mdot_out_A"], ["mdot_in", "mdot_split_fraction"], rows=rng, cols=rng) + self.declare_partials(["mdot_out_B"], ["mdot_in", "mdot_split_fraction"], rows=rng, cols=rng) - self.declare_partials(['mdot_out_A'], ['mdot_in', 'mdot_split_fraction'], rows=rng, cols=rng) - self.declare_partials(['mdot_out_B'], ['mdot_in', 'mdot_split_fraction'], rows=rng, cols=rng) - def compute(self, inputs, outputs): - if np.any(inputs['mdot_split_fraction'] < 0) or np.any(inputs['mdot_split_fraction'] > 1): - raise RuntimeWarning(f"mdot_split_fraction of {inputs['mdot_split_fraction']} has at least one element out of range [0, 1]") - outputs['mdot_out_A'] = inputs['mdot_in'] * inputs['mdot_split_fraction'] - outputs['mdot_out_B'] = inputs['mdot_in'] * (1 - inputs['mdot_split_fraction']) + if np.any(inputs["mdot_split_fraction"] < 0) or np.any(inputs["mdot_split_fraction"] > 1): + raise RuntimeWarning( + f"mdot_split_fraction of {inputs['mdot_split_fraction']} has at least one element out of range [0, 1]" + ) + outputs["mdot_out_A"] = inputs["mdot_in"] * inputs["mdot_split_fraction"] + outputs["mdot_out_B"] = inputs["mdot_in"] * (1 - inputs["mdot_split_fraction"]) def compute_partials(self, inputs, J): - J['mdot_out_A', 'mdot_in'] = inputs['mdot_split_fraction'] - J['mdot_out_A', 'mdot_split_fraction'] = inputs['mdot_in'] + J["mdot_out_A", "mdot_in"] = inputs["mdot_split_fraction"] + J["mdot_out_A", "mdot_split_fraction"] = inputs["mdot_in"] - J['mdot_out_B', 'mdot_in'] = 1 - inputs['mdot_split_fraction'] - J['mdot_out_B', 'mdot_split_fraction'] = - inputs['mdot_in'] + J["mdot_out_B", "mdot_in"] = 1 - inputs["mdot_split_fraction"] + J["mdot_out_B", "mdot_split_fraction"] = -inputs["mdot_in"] class FlowCombine(ExplicitComponent): @@ -84,42 +87,43 @@ class FlowCombine(ExplicitComponent): num_nodes : int Number of analysis points (scalar, default 1) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] rng = np.arange(0, nn) - self.add_input('mdot_in_A', units='kg/s', shape=(nn,)) - self.add_input('mdot_in_B', units='kg/s', shape=(nn,)) - self.add_input('T_in_A', units='K', shape=(nn,)) - self.add_input('T_in_B', units='K', shape=(nn,)) + self.add_input("mdot_in_A", units="kg/s", shape=(nn,)) + self.add_input("mdot_in_B", units="kg/s", shape=(nn,)) + self.add_input("T_in_A", units="K", shape=(nn,)) + self.add_input("T_in_B", units="K", shape=(nn,)) + + self.add_output("mdot_out", units="kg/s", shape=(nn,)) + self.add_output("T_out", units="K", shape=(nn,)) - self.add_output('mdot_out', units='kg/s', shape=(nn,)) - self.add_output('T_out', units='K', shape=(nn,)) + self.declare_partials(["mdot_out"], ["mdot_in_A", "mdot_in_B"], rows=rng, cols=rng) + self.declare_partials(["T_out"], ["mdot_in_A", "mdot_in_B", "T_in_A", "T_in_B"], rows=rng, cols=rng) - self.declare_partials(['mdot_out'], ['mdot_in_A', 'mdot_in_B'], rows=rng, cols=rng) - self.declare_partials(['T_out'], ['mdot_in_A', 'mdot_in_B', 'T_in_A', 'T_in_B'], rows=rng, cols=rng) - def compute(self, inputs, outputs): - mdot_A = inputs['mdot_in_A'] - mdot_B = inputs['mdot_in_B'] - outputs['mdot_out'] = mdot_A + mdot_B + mdot_A = inputs["mdot_in_A"] + mdot_B = inputs["mdot_in_B"] + outputs["mdot_out"] = mdot_A + mdot_B # Weighted average of temperatures for output temperature - outputs['T_out'] = (mdot_A * inputs['T_in_A'] + mdot_B * inputs['T_in_B']) / (mdot_A + mdot_B) + outputs["T_out"] = (mdot_A * inputs["T_in_A"] + mdot_B * inputs["T_in_B"]) / (mdot_A + mdot_B) def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - J['mdot_out', 'mdot_in_A'] = np.ones((nn,)) - J['mdot_out', 'mdot_in_B'] = np.ones((nn,)) + nn = self.options["num_nodes"] + J["mdot_out", "mdot_in_A"] = np.ones((nn,)) + J["mdot_out", "mdot_in_B"] = np.ones((nn,)) - mdot_A = inputs['mdot_in_A'] - mdot_B = inputs['mdot_in_B'] + mdot_A = inputs["mdot_in_A"] + mdot_B = inputs["mdot_in_B"] mdot = mdot_A + mdot_B - T_A = inputs['T_in_A'] - T_B = inputs['T_in_B'] - J['T_out', 'mdot_in_A'] = (mdot * T_A - mdot_A * T_A - mdot_B * T_B) / (mdot**2) - J['T_out', 'mdot_in_B'] = (mdot * T_B - mdot_A * T_A - mdot_B * T_B) / (mdot**2) - J['T_out', 'T_in_A'] = mdot_A / mdot - J['T_out', 'T_in_B'] = mdot_B / mdot \ No newline at end of file + T_A = inputs["T_in_A"] + T_B = inputs["T_in_B"] + J["T_out", "mdot_in_A"] = (mdot * T_A - mdot_A * T_A - mdot_B * T_B) / (mdot**2) + J["T_out", "mdot_in_B"] = (mdot * T_B - mdot_A * T_A - mdot_B * T_B) / (mdot**2) + J["T_out", "T_in_A"] = mdot_A / mdot + J["T_out", "T_in_B"] = mdot_B / mdot diff --git a/openconcept/thermal/motor_cooling.py b/openconcept/thermal/motor_cooling.py index 614100cf..bcf0c7d2 100644 --- a/openconcept/thermal/motor_cooling.py +++ b/openconcept/thermal/motor_cooling.py @@ -2,6 +2,7 @@ import numpy as np from openconcept.utilities import Integrator + class LiquidCooledMotor(om.Group): """A component (heat producing) with thermal mass cooled by a cold plate. @@ -46,35 +47,50 @@ class LiquidCooledMotor(om.Group): """ def initialize(self): - self.options.declare('motor_specific_heat', default=921.0, desc='Specific heat in J/kg/K') - self.options.declare('coolant_specific_heat', default=3801, desc='Specific heat in J/kg/K') - self.options.declare('quasi_steady', default=False, desc='Treat the component as quasi-steady or with thermal mass') - self.options.declare('num_nodes', default=1, desc='Number of quasi-steady points to runs') - self.options.declare('case_cooling_coefficient', default=1100.) + self.options.declare("motor_specific_heat", default=921.0, desc="Specific heat in J/kg/K") + self.options.declare("coolant_specific_heat", default=3801, desc="Specific heat in J/kg/K") + self.options.declare( + "quasi_steady", default=False, desc="Treat the component as quasi-steady or with thermal mass" + ) + self.options.declare("num_nodes", default=1, desc="Number of quasi-steady points to runs") + self.options.declare("case_cooling_coefficient", default=1100.0) def setup(self): - nn = self.options['num_nodes'] - quasi_steady = self.options['quasi_steady'] - self.add_subsystem('hex', - MotorCoolingJacket(num_nodes=nn, coolant_specific_heat=self.options['coolant_specific_heat'], - motor_specific_heat=self.options['motor_specific_heat'], - case_cooling_coefficient=self.options['case_cooling_coefficient']), - promotes_inputs=['q_in','T_in', 'T','power_rating','mdot_coolant','motor_weight'], - promotes_outputs=['T_out', 'dTdt']) + nn = self.options["num_nodes"] + quasi_steady = self.options["quasi_steady"] + self.add_subsystem( + "hex", + MotorCoolingJacket( + num_nodes=nn, + coolant_specific_heat=self.options["coolant_specific_heat"], + motor_specific_heat=self.options["motor_specific_heat"], + case_cooling_coefficient=self.options["case_cooling_coefficient"], + ), + promotes_inputs=["q_in", "T_in", "T", "power_rating", "mdot_coolant", "motor_weight"], + promotes_outputs=["T_out", "dTdt"], + ) if not quasi_steady: - ode_integ = self.add_subsystem('ode_integ', Integrator(num_nodes=nn, diff_units='s', method='simpson', time_setup='duration'), - promotes_outputs=['*'], promotes_inputs=['*']) - ode_integ.add_integrand('T', rate_name='dTdt', units='K', lower=1e-10) + ode_integ = self.add_subsystem( + "ode_integ", + Integrator(num_nodes=nn, diff_units="s", method="simpson", time_setup="duration"), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + ode_integ.add_integrand("T", rate_name="dTdt", units="K", lower=1e-10) else: - self.add_subsystem('thermal_bal', - om.BalanceComp('T', eq_units='K/s', lhs_name='dTdt', rhs_val=0.0, units='K', lower=1.0, val=299.*np.ones((nn,))), - promotes_inputs=['dTdt'], - promotes_outputs=['T']) + self.add_subsystem( + "thermal_bal", + om.BalanceComp( + "T", eq_units="K/s", lhs_name="dTdt", rhs_val=0.0, units="K", lower=1.0, val=299.0 * np.ones((nn,)) + ), + promotes_inputs=["dTdt"], + promotes_outputs=["T"], + ) class MotorCoolingJacket(om.ExplicitComponent): """ - Computes motor winding temperature assuming + Computes motor winding temperature assuming well-designed, high-power-density aerospace motor. This component is based on the following assumptions: - 2020 technology level @@ -92,15 +108,15 @@ class MotorCoolingJacket(om.ExplicitComponent): Magni500: 560kW rated power, ~0.652m OD, 0.4m case "depth" Siemens SP200D: 200kW rated power, ~0.63m OD, ~0.16 case "depth" - Based on these dimensions I assume 650kW per square meter - of casing surface area. This includes only the cylindrical portion, + Based on these dimensions I assume 650kW per square meter + of casing surface area. This includes only the cylindrical portion, not the front and rear motor faces. - Using a thermal FEM image of the SP200D, I estimate - a temperature rise of 23K from coolant inlet temperature (~85C) - to winding max temp (~108C) at the steady state operating point. + Using a thermal FEM image of the SP200D, I estimate + a temperature rise of 23K from coolant inlet temperature (~85C) + to winding max temp (~108C) at the steady state operating point. With 95% efficiency at 200kW, this is about 1373 W / m^2 casing area / K. - We'll reduce that somewhat since this is a direct oil cooling system, + We'll reduce that somewhat since this is a direct oil cooling system, and assume 1100 W/m^2/K instead. Dividing 1.1 kW/m^2/K by 650kWrated/m^2 gives: 1.69e-3 kW / kWrated / K @@ -134,7 +150,7 @@ class MotorCoolingJacket(om.ExplicitComponent): Heat transfer rate from the motor to the fluid (vector, W) T_out : float Outlet fluid temperature (vector, K) - + Options ------- @@ -151,71 +167,96 @@ class MotorCoolingJacket(om.ExplicitComponent): motor_specific_heat : float Specific heat of the motor casing (J/kg/K) (default 921, alu) """ - + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - self.options.declare('coolant_specific_heat', default=3801, desc='Specific heat in J/kg/K') - self.options.declare('case_cooling_coefficient', default=1100.) - self.options.declare('case_area_coefficient', default=650000.) - self.options.declare('motor_specific_heat', default=921, desc='Specific heat in J/kg/K - default 921 for aluminum') - + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + self.options.declare("coolant_specific_heat", default=3801, desc="Specific heat in J/kg/K") + self.options.declare("case_cooling_coefficient", default=1100.0) + self.options.declare("case_area_coefficient", default=650000.0) + self.options.declare( + "motor_specific_heat", default=921, desc="Specific heat in J/kg/K - default 921 for aluminum" + ) + def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] arange = np.arange(nn) - self.add_input('q_in', shape=(nn,), units='W', val=0.0) - self.add_input('T_in', shape=(nn,), units='K', val=330) - self.add_input('T', shape=(nn,), units='K', val=359.546) - self.add_input('mdot_coolant', shape=(nn,), units='kg/s', val=1.0) - self.add_input('power_rating', units='W', val=2e5) - self.add_input('motor_weight', units='kg', val=100) - self.add_output('q', shape=(nn,), units='W') - self.add_output('T_out', shape=(nn,), units='K', val=300, lower=1e-10) - self.add_output('dTdt', shape=(nn,), units='K/s', tags=['integrate', 'state_name:T_motor', 'state_units:K', 'state_val:300.0', 'state_promotes:True']) - - self.declare_partials(['T_out','q','dTdt'], ['power_rating'], rows=arange, cols=np.zeros((nn,))) - self.declare_partials(['dTdt'], ['motor_weight'], rows=arange, cols=np.zeros((nn,))) - - self.declare_partials(['T_out','q','dTdt'], ['T_in', 'T','mdot_coolant'], rows=arange, cols=arange) - self.declare_partials(['dTdt'], ['q_in'], rows=arange, cols=arange) + self.add_input("q_in", shape=(nn,), units="W", val=0.0) + self.add_input("T_in", shape=(nn,), units="K", val=330) + self.add_input("T", shape=(nn,), units="K", val=359.546) + self.add_input("mdot_coolant", shape=(nn,), units="kg/s", val=1.0) + self.add_input("power_rating", units="W", val=2e5) + self.add_input("motor_weight", units="kg", val=100) + self.add_output("q", shape=(nn,), units="W") + self.add_output("T_out", shape=(nn,), units="K", val=300, lower=1e-10) + self.add_output( + "dTdt", + shape=(nn,), + units="K/s", + tags=["integrate", "state_name:T_motor", "state_units:K", "state_val:300.0", "state_promotes:True"], + ) + + self.declare_partials(["T_out", "q", "dTdt"], ["power_rating"], rows=arange, cols=np.zeros((nn,))) + self.declare_partials(["dTdt"], ["motor_weight"], rows=arange, cols=np.zeros((nn,))) + + self.declare_partials(["T_out", "q", "dTdt"], ["T_in", "T", "mdot_coolant"], rows=arange, cols=arange) + self.declare_partials(["dTdt"], ["q_in"], rows=arange, cols=arange) def compute(self, inputs, outputs): - const = self.options['case_cooling_coefficient'] / self.options['case_area_coefficient'] - - NTU = const * inputs['power_rating'] / inputs['mdot_coolant'] / self.options['coolant_specific_heat'] + const = self.options["case_cooling_coefficient"] / self.options["case_area_coefficient"] + + NTU = const * inputs["power_rating"] / inputs["mdot_coolant"] / self.options["coolant_specific_heat"] effectiveness = 1 - np.exp(-NTU) - heat_transfer = (inputs['T'] - inputs['T_in']) * effectiveness * inputs['mdot_coolant'] * self.options['coolant_specific_heat'] - outputs['q'] = heat_transfer - outputs['T_out'] = inputs['T_in'] + heat_transfer / inputs['mdot_coolant'] / self.options['coolant_specific_heat'] - outputs['dTdt'] = (inputs['q_in'] - outputs['q']) / inputs['motor_weight'] / self.options['motor_specific_heat'] - + heat_transfer = ( + (inputs["T"] - inputs["T_in"]) + * effectiveness + * inputs["mdot_coolant"] + * self.options["coolant_specific_heat"] + ) + outputs["q"] = heat_transfer + outputs["T_out"] = ( + inputs["T_in"] + heat_transfer / inputs["mdot_coolant"] / self.options["coolant_specific_heat"] + ) + outputs["dTdt"] = (inputs["q_in"] - outputs["q"]) / inputs["motor_weight"] / self.options["motor_specific_heat"] + def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - cp = self.options['coolant_specific_heat'] - mdot = inputs['mdot_coolant'] - const = self.options['case_cooling_coefficient'] / self.options['case_area_coefficient'] - - NTU = const * inputs['power_rating'] / mdot / cp - dNTU_dP = const / mdot / cp - dNTU_dmdot = -const * inputs['power_rating'] / mdot **2 / cp + nn = self.options["num_nodes"] + cp = self.options["coolant_specific_heat"] + mdot = inputs["mdot_coolant"] + const = self.options["case_cooling_coefficient"] / self.options["case_area_coefficient"] + + NTU = const * inputs["power_rating"] / mdot / cp + dNTU_dP = const / mdot / cp + dNTU_dmdot = -const * inputs["power_rating"] / mdot**2 / cp effectiveness = 1 - np.exp(-NTU) deff_dP = np.exp(-NTU) * dNTU_dP deff_dmdot = np.exp(-NTU) * dNTU_dmdot - heat_transfer = (inputs['T'] - inputs['T_in']) * effectiveness * inputs['mdot_coolant'] * self.options['coolant_specific_heat'] + heat_transfer = ( + (inputs["T"] - inputs["T_in"]) + * effectiveness + * inputs["mdot_coolant"] + * self.options["coolant_specific_heat"] + ) - J['q', 'T'] = effectiveness * mdot * cp - J['q', 'T_in'] = - effectiveness * mdot * cp - J['q', 'power_rating'] = (inputs['T'] - inputs['T_in']) * deff_dP * mdot * cp - J['q', 'mdot_coolant'] = (inputs['T'] - inputs['T_in']) * cp * (effectiveness + deff_dmdot * mdot) + J["q", "T"] = effectiveness * mdot * cp + J["q", "T_in"] = -effectiveness * mdot * cp + J["q", "power_rating"] = (inputs["T"] - inputs["T_in"]) * deff_dP * mdot * cp + J["q", "mdot_coolant"] = (inputs["T"] - inputs["T_in"]) * cp * (effectiveness + deff_dmdot * mdot) - J['T_out', 'T'] = J['q','T'] / mdot / cp - J['T_out', 'T_in'] = np.ones(nn) + J['q','T_in'] / mdot / cp - J['T_out', 'power_rating'] = J['q', 'power_rating'] / mdot / cp - J['T_out', 'mdot_coolant'] = (J['q', 'mdot_coolant'] * mdot - heat_transfer) / cp / mdot ** 2 + J["T_out", "T"] = J["q", "T"] / mdot / cp + J["T_out", "T_in"] = np.ones(nn) + J["q", "T_in"] / mdot / cp + J["T_out", "power_rating"] = J["q", "power_rating"] / mdot / cp + J["T_out", "mdot_coolant"] = (J["q", "mdot_coolant"] * mdot - heat_transfer) / cp / mdot**2 - J['dTdt', 'q_in'] = 1 / inputs['motor_weight'] / self.options['motor_specific_heat'] - J['dTdt', 'T'] = -J['q', 'T'] / inputs['motor_weight'] / self.options['motor_specific_heat'] - J['dTdt', 'T_in'] = -J['q', 'T_in'] / inputs['motor_weight'] / self.options['motor_specific_heat'] - J['dTdt', 'power_rating'] = -J['q', 'power_rating'] / inputs['motor_weight'] / self.options['motor_specific_heat'] - J['dTdt', 'mdot_coolant'] = -J['q', 'mdot_coolant'] / inputs['motor_weight'] / self.options['motor_specific_heat'] - J['dTdt', 'motor_weight'] = -(inputs['q_in'] - heat_transfer) / inputs['motor_weight']**2 / self.options['motor_specific_heat'] + J["dTdt", "q_in"] = 1 / inputs["motor_weight"] / self.options["motor_specific_heat"] + J["dTdt", "T"] = -J["q", "T"] / inputs["motor_weight"] / self.options["motor_specific_heat"] + J["dTdt", "T_in"] = -J["q", "T_in"] / inputs["motor_weight"] / self.options["motor_specific_heat"] + J["dTdt", "power_rating"] = ( + -J["q", "power_rating"] / inputs["motor_weight"] / self.options["motor_specific_heat"] + ) + J["dTdt", "mdot_coolant"] = ( + -J["q", "mdot_coolant"] / inputs["motor_weight"] / self.options["motor_specific_heat"] + ) + J["dTdt", "motor_weight"] = ( + -(inputs["q_in"] - heat_transfer) / inputs["motor_weight"] ** 2 / self.options["motor_specific_heat"] + ) diff --git a/openconcept/thermal/pump.py b/openconcept/thermal/pump.py index 6e334ede..ea2e5196 100644 --- a/openconcept/thermal/pump.py +++ b/openconcept/thermal/pump.py @@ -1,10 +1,11 @@ import openmdao.api as om import numpy as np + class SimplePump(om.ExplicitComponent): """ A pump that circulates coolant against pressure. - The default parameters are based on a survey of commercial + The default parameters are based on a survey of commercial airplane fuel pumps of a variety of makes and models. Inputs @@ -38,54 +39,59 @@ class SimplePump(om.ExplicitComponent): weight_inc : float Incremental weight of pump, scales linearly with power rating (default 1/450 kg/W) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - self.options.declare('efficiency', default=0.35, desc='Efficiency (dimensionless)') - self.options.declare('weight_base', default=0.0, desc='Pump base weight') - self.options.declare('weight_inc', default=1/450, desc='Incremental pump weight (kg/W)') + self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions") + self.options.declare("efficiency", default=0.35, desc="Efficiency (dimensionless)") + self.options.declare("weight_base", default=0.0, desc="Pump base weight") + self.options.declare("weight_inc", default=1 / 450, desc="Incremental pump weight (kg/W)") def setup(self): - nn = self.options['num_nodes'] - eta = self.options['efficiency'] - weight_inc = self.options['weight_inc'] - - self.add_input('power_rating', units='W', desc='Pump electrical power rating') - self.add_input('mdot_coolant', units='kg/s', desc='Coolant mass flow rate', val=np.ones((nn,))) - self.add_input('delta_p', units='Pa', desc='Pump pressure rise', val=np.ones((nn,))) - self.add_input('rho_coolant', units='kg/m**3', desc='Coolant density', val=np.ones((nn,))) + nn = self.options["num_nodes"] + eta = self.options["efficiency"] + weight_inc = self.options["weight_inc"] - self.add_output('elec_load', units='W', desc='Pump electrical load', val=np.ones((nn,))) - self.add_output('component_weight', units='kg', desc='Pump weight') - self.add_output('component_sizing_margin', units=None, val=np.ones((nn,)), desc='Comp sizing margin') - - self.declare_partials(['elec_load','component_sizing_margin'], ['rho_coolant', 'delta_p', 'mdot_coolant'], rows=np.arange(nn), cols=np.arange(nn)) - self.declare_partials(['component_sizing_margin'], ['power_rating'], rows=np.arange(nn), cols=np.zeros(nn)) - self.declare_partials(['component_weight'], ['power_rating'], val=weight_inc) + self.add_input("power_rating", units="W", desc="Pump electrical power rating") + self.add_input("mdot_coolant", units="kg/s", desc="Coolant mass flow rate", val=np.ones((nn,))) + self.add_input("delta_p", units="Pa", desc="Pump pressure rise", val=np.ones((nn,))) + self.add_input("rho_coolant", units="kg/m**3", desc="Coolant density", val=np.ones((nn,))) + self.add_output("elec_load", units="W", desc="Pump electrical load", val=np.ones((nn,))) + self.add_output("component_weight", units="kg", desc="Pump weight") + self.add_output("component_sizing_margin", units=None, val=np.ones((nn,)), desc="Comp sizing margin") + self.declare_partials( + ["elec_load", "component_sizing_margin"], + ["rho_coolant", "delta_p", "mdot_coolant"], + rows=np.arange(nn), + cols=np.arange(nn), + ) + self.declare_partials(["component_sizing_margin"], ["power_rating"], rows=np.arange(nn), cols=np.zeros(nn)) + self.declare_partials(["component_weight"], ["power_rating"], val=weight_inc) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - eta = self.options['efficiency'] - weight_inc = self.options['weight_inc'] - weight_base = self.options['weight_base'] + nn = self.options["num_nodes"] + eta = self.options["efficiency"] + weight_inc = self.options["weight_inc"] + weight_base = self.options["weight_base"] + + outputs["component_weight"] = weight_base + weight_inc * inputs["power_rating"] - outputs['component_weight'] = weight_base + weight_inc * inputs['power_rating'] - # compute the fluid power - vol_flow_rate = inputs['mdot_coolant'] / inputs['rho_coolant'] # m3/s - fluid_power = vol_flow_rate * inputs['delta_p'] - outputs['elec_load'] = fluid_power / eta - outputs['component_sizing_margin'] = outputs['elec_load'] / inputs['power_rating'] - + vol_flow_rate = inputs["mdot_coolant"] / inputs["rho_coolant"] # m3/s + fluid_power = vol_flow_rate * inputs["delta_p"] + outputs["elec_load"] = fluid_power / eta + outputs["component_sizing_margin"] = outputs["elec_load"] / inputs["power_rating"] def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - eta = self.options['efficiency'] + nn = self.options["num_nodes"] + eta = self.options["efficiency"] - J['elec_load', 'mdot_coolant'] = inputs['delta_p'] / inputs['rho_coolant'] / eta - J['elec_load', 'delta_p'] = inputs['mdot_coolant'] / inputs['rho_coolant'] / eta - J['elec_load', 'rho_coolant'] = -inputs['mdot_coolant'] * inputs['delta_p'] / inputs['rho_coolant'] ** 2 / eta - for in_var in ['mdot_coolant', 'delta_p', 'rho_coolant']: - J['component_sizing_margin', in_var] = J['elec_load', in_var] / inputs['power_rating'] - J['component_sizing_margin', 'power_rating'] = - inputs['mdot_coolant'] * inputs['delta_p'] / inputs['rho_coolant'] / eta / inputs['power_rating'] ** 2 + J["elec_load", "mdot_coolant"] = inputs["delta_p"] / inputs["rho_coolant"] / eta + J["elec_load", "delta_p"] = inputs["mdot_coolant"] / inputs["rho_coolant"] / eta + J["elec_load", "rho_coolant"] = -inputs["mdot_coolant"] * inputs["delta_p"] / inputs["rho_coolant"] ** 2 / eta + for in_var in ["mdot_coolant", "delta_p", "rho_coolant"]: + J["component_sizing_margin", in_var] = J["elec_load", in_var] / inputs["power_rating"] + J["component_sizing_margin", "power_rating"] = ( + -inputs["mdot_coolant"] * inputs["delta_p"] / inputs["rho_coolant"] / eta / inputs["power_rating"] ** 2 + ) diff --git a/openconcept/thermal/tests/test_battery_cooling.py b/openconcept/thermal/tests/test_battery_cooling.py index aa3a395d..dda5a756 100644 --- a/openconcept/thermal/tests/test_battery_cooling.py +++ b/openconcept/thermal/tests/test_battery_cooling.py @@ -9,14 +9,15 @@ class QuasiSteadyBatteryCoolingTestCase(unittest.TestCase): """ Test the liquid cooled battery in quasi-steady (massless) mode """ + def generate_model(self, nn): prob = Problem() - iv = prob.model.add_subsystem('iv', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('q_in', val=np.linspace(2000,5000,nn), units='W') - iv.add_output('mdot_coolant', val=1*np.ones((nn,)), units='kg/s') - iv.add_output('T_in', val=25*np.ones((nn,)), units='degC') - iv.add_output('battery_weight', val=100., units='kg') - prob.model.add_subsystem('test', LiquidCooledBattery(num_nodes=nn, quasi_steady=True), promotes=['*']) + iv = prob.model.add_subsystem("iv", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("q_in", val=np.linspace(2000, 5000, nn), units="W") + iv.add_output("mdot_coolant", val=1 * np.ones((nn,)), units="kg/s") + iv.add_output("T_in", val=25 * np.ones((nn,)), units="degC") + iv.add_output("battery_weight", val=100.0, units="kg") + prob.model.add_subsystem("test", LiquidCooledBattery(num_nodes=nn, quasi_steady=True), promotes=["*"]) prob.model.nonlinear_solver = NewtonSolver(solve_subsystems=True) prob.model.linear_solver = DirectSolver() prob.setup(check=True, force_alloc_complex=True) @@ -25,46 +26,112 @@ def generate_model(self, nn): def test_scalar(self): prob = self.generate_model(nn=1) prob.run_model() - assert_near_equal(prob.get_val('dTdt'), 0.0, tolerance=1e-14) - assert_near_equal(prob.get_val('T_surface', units='K'), 298.94004878, tolerance=1e-10) - assert_near_equal(prob.get_val('T_core', units='K'), 307.10184074, tolerance=1e-10) - assert_near_equal(prob.get_val('test.hex.q', units='W'), 2000.0, tolerance=1e-10) - assert_near_equal(prob.get_val('T_out', units='K'), 298.6761773, tolerance=1e-10) - assert_near_equal(prob.get_val('T', units='K'), 303.02094476, tolerance=1e-10) - - partials = prob.check_partials(method='cs',compact_print=True, step=1e-50) + assert_near_equal(prob.get_val("dTdt"), 0.0, tolerance=1e-14) + assert_near_equal(prob.get_val("T_surface", units="K"), 298.94004878, tolerance=1e-10) + assert_near_equal(prob.get_val("T_core", units="K"), 307.10184074, tolerance=1e-10) + assert_near_equal(prob.get_val("test.hex.q", units="W"), 2000.0, tolerance=1e-10) + assert_near_equal(prob.get_val("T_out", units="K"), 298.6761773, tolerance=1e-10) + assert_near_equal(prob.get_val("T", units="K"), 303.02094476, tolerance=1e-10) + + partials = prob.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) def test_vector(self): prob = self.generate_model(nn=11) prob.run_model() - assert_near_equal(prob.get_val('dTdt'), np.zeros((11,)), tolerance=1e-14) - assert_near_equal(prob.get_val('T_surface', units='K'), - np.array([333.94004878, 334.0585561 , 334.17706342, 334.29557074, - 334.41407805, 334.53258537, 334.65109269, 334.76960001, - 334.88810732, 335.00661464, 335.12512196])-35., tolerance=1e-10) - assert_near_equal(prob.get_val('T_core', units='K'), - np.array([342.10184074, 343.44461685, 344.78739296, 346.13016907, - 347.47294518, 348.81572129, 350.1584974 , 351.50127351, - 352.84404962, 354.18682573, 355.52960184])-35., tolerance=1e-10) - assert_near_equal(prob.get_val('test.hex.q', units='W'), np.linspace(2000,5000,11), tolerance=1e-10) - assert_near_equal(prob.get_val('T_out', units='K'), - np.array([333.67617732, 333.75510392, 333.83403052, 333.91295712, - 333.99188371, 334.07081031, 334.14973691, 334.22866351, - 334.30759011, 334.38651671, 334.4654433 ])-35., tolerance=1e-10) - assert_near_equal(prob.get_val('T', units='K'), - np.array([338.02094476, 338.75158647, 339.48222819, 340.2128699 , - 340.94351162, 341.67415333, 342.40479505, 343.13543676, - 343.86607847, 344.59672019, 345.3273619])-35., tolerance=1e-10) + assert_near_equal(prob.get_val("dTdt"), np.zeros((11,)), tolerance=1e-14) + assert_near_equal( + prob.get_val("T_surface", units="K"), + np.array( + [ + 333.94004878, + 334.0585561, + 334.17706342, + 334.29557074, + 334.41407805, + 334.53258537, + 334.65109269, + 334.76960001, + 334.88810732, + 335.00661464, + 335.12512196, + ] + ) + - 35.0, + tolerance=1e-10, + ) + assert_near_equal( + prob.get_val("T_core", units="K"), + np.array( + [ + 342.10184074, + 343.44461685, + 344.78739296, + 346.13016907, + 347.47294518, + 348.81572129, + 350.1584974, + 351.50127351, + 352.84404962, + 354.18682573, + 355.52960184, + ] + ) + - 35.0, + tolerance=1e-10, + ) + assert_near_equal(prob.get_val("test.hex.q", units="W"), np.linspace(2000, 5000, 11), tolerance=1e-10) + assert_near_equal( + prob.get_val("T_out", units="K"), + np.array( + [ + 333.67617732, + 333.75510392, + 333.83403052, + 333.91295712, + 333.99188371, + 334.07081031, + 334.14973691, + 334.22866351, + 334.30759011, + 334.38651671, + 334.4654433, + ] + ) + - 35.0, + tolerance=1e-10, + ) + assert_near_equal( + prob.get_val("T", units="K"), + np.array( + [ + 338.02094476, + 338.75158647, + 339.48222819, + 340.2128699, + 340.94351162, + 341.67415333, + 342.40479505, + 343.13543676, + 343.86607847, + 344.59672019, + 345.3273619, + ] + ) + - 35.0, + tolerance=1e-10, + ) # prob.model.list_outputs(print_arrays=True, units='True') - partials = prob.check_partials(method='cs',compact_print=True, step=1e-50) + partials = prob.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) + class UnsteadyBatteryCoolingTestCase(unittest.TestCase): """ Test the liquid cooled battery in unsteady mode """ + def generate_model(self, nn): """ An example demonstrating unsteady battery cooling @@ -75,37 +142,38 @@ def generate_model(self, nn): class VehicleModel(Group): def initialize(self): - self.options.declare('num_nodes', default=11) + self.options.declare("num_nodes", default=11) def setup(self): - num_nodes = self.options['num_nodes'] - ivc = self.add_subsystem('ivc', IndepVarComp(), promotes_outputs=['*']) - ivc.add_output('battery_heat', val=np.ones((num_nodes,))*5000, units='W') - ivc.add_output('coolant_temp', 25.*np.ones((num_nodes,)), units='degC') - ivc.add_output('mdot_coolant', 1.0*np.ones((num_nodes,)), units='kg/s') - ivc.add_output('battery_weight', 100, units='kg') - ivc.add_output('n_cpb', 21) - ivc.add_output('t_channel', 0.0005, units='m') - - - self.add_subsystem('bcs', LiquidCooledBattery(num_nodes=num_nodes, quasi_steady=False)) - self.connect('battery_heat', 'bcs.q_in') - self.connect('coolant_temp', 'bcs.T_in') - self.connect('mdot_coolant', 'bcs.mdot_coolant') - self.connect('battery_weight', 'bcs.battery_weight') - self.connect('n_cpb', 'bcs.n_cpb') - self.connect('t_channel', 'bcs.t_channel') + num_nodes = self.options["num_nodes"] + ivc = self.add_subsystem("ivc", IndepVarComp(), promotes_outputs=["*"]) + ivc.add_output("battery_heat", val=np.ones((num_nodes,)) * 5000, units="W") + ivc.add_output("coolant_temp", 25.0 * np.ones((num_nodes,)), units="degC") + ivc.add_output("mdot_coolant", 1.0 * np.ones((num_nodes,)), units="kg/s") + ivc.add_output("battery_weight", 100, units="kg") + ivc.add_output("n_cpb", 21) + ivc.add_output("t_channel", 0.0005, units="m") + + self.add_subsystem("bcs", LiquidCooledBattery(num_nodes=num_nodes, quasi_steady=False)) + self.connect("battery_heat", "bcs.q_in") + self.connect("coolant_temp", "bcs.T_in") + self.connect("mdot_coolant", "bcs.mdot_coolant") + self.connect("battery_weight", "bcs.battery_weight") + self.connect("n_cpb", "bcs.n_cpb") + self.connect("t_channel", "bcs.t_channel") class TrajectoryPhase(PhaseGroup): "An OpenConcept Phase comprises part of a time-based TrajectoryGroup and always needs to have a 'duration' defined" + def setup(self): - self.add_subsystem('ivc', IndepVarComp('duration', val=30, units='min'), promotes_outputs=['duration']) - self.add_subsystem('vm', VehicleModel(num_nodes=self.options['num_nodes'])) + self.add_subsystem("ivc", IndepVarComp("duration", val=30, units="min"), promotes_outputs=["duration"]) + self.add_subsystem("vm", VehicleModel(num_nodes=self.options["num_nodes"])) class Trajectory(TrajectoryGroup): "An OpenConcept TrajectoryGroup consists of one or more phases that may be linked together. This will often be a top-level model" + def setup(self): - self.add_subsystem('phase1', TrajectoryPhase(num_nodes=nn)) + self.add_subsystem("phase1", TrajectoryPhase(num_nodes=nn)) # self.add_subsystem('phase2', TrajectoryPhase(num_nodes=nn)) # the link_phases directive ensures continuity of state variables across phase boundaries # self.link_phases(self.phase1, self.phase2) @@ -113,39 +181,99 @@ def setup(self): prob = Problem(Trajectory()) prob.model.nonlinear_solver = NewtonSolver(iprint=2) prob.model.linear_solver = DirectSolver() - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.nonlinear_solver.options['maxiter'] = 20 - prob.model.nonlinear_solver.options['atol'] = 1e-6 - prob.model.nonlinear_solver.options['rtol'] = 1e-6 + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.nonlinear_solver.options["maxiter"] = 20 + prob.model.nonlinear_solver.options["atol"] = 1e-6 + prob.model.nonlinear_solver.options["rtol"] = 1e-6 prob.setup(force_alloc_complex=True) # set the initial value of the state at the beginning of the TrajectoryGroup - prob['phase1.vm.bcs.T_initial'] = 300. + prob["phase1.vm.bcs.T_initial"] = 300.0 prob.run_model() # prob.model.list_outputs(print_arrays=True, units=True) # prob.model.list_inputs(print_arrays=True, units=True) - + return prob def test_vector(self): prob = self.generate_model(nn=11) prob.run_model() - assert_near_equal(prob.get_val('phase1.vm.bcs.T_surface', units='K'), - np.array([298.45006299, 299.70461767, 299.97097736, 300.08642573, - 300.11093705, 300.121561 , 300.12381662, 300.12479427, - 300.12500184, 300.1250918 , 300.12511091]), tolerance=1e-10) - assert_near_equal(prob.get_val('phase1.vm.bcs.T_core', units='K'), - np.array([301.54993701, 315.76497532, 318.78302876, 320.09114476, - 320.36887627, 320.48925354, 320.51481133, 320.52588886, - 320.52824077, 320.52926016, 320.52947659]), tolerance=1e-10) - assert_near_equal(prob.get_val('phase1.vm.bcs.T_out', units='K'), - np.array([298.34984379, 299.18538488, 299.36278206, 299.43967138, - 299.45599607, 299.46307168, 299.46457394, 299.46522506, - 299.4653633 , 299.46542322, 299.46543594]), tolerance=1e-10) - assert_near_equal(prob.get_val('phase1.vm.bcs.T', units='K'), - np.array([300. , 307.73479649, 309.37700306, 310.08878525, - 310.23990666, 310.30540727, 310.31931397, 310.32534156, - 310.3266213 , 310.32717598, 310.32729375]), tolerance=1e-10) + assert_near_equal( + prob.get_val("phase1.vm.bcs.T_surface", units="K"), + np.array( + [ + 298.45006299, + 299.70461767, + 299.97097736, + 300.08642573, + 300.11093705, + 300.121561, + 300.12381662, + 300.12479427, + 300.12500184, + 300.1250918, + 300.12511091, + ] + ), + tolerance=1e-10, + ) + assert_near_equal( + prob.get_val("phase1.vm.bcs.T_core", units="K"), + np.array( + [ + 301.54993701, + 315.76497532, + 318.78302876, + 320.09114476, + 320.36887627, + 320.48925354, + 320.51481133, + 320.52588886, + 320.52824077, + 320.52926016, + 320.52947659, + ] + ), + tolerance=1e-10, + ) + assert_near_equal( + prob.get_val("phase1.vm.bcs.T_out", units="K"), + np.array( + [ + 298.34984379, + 299.18538488, + 299.36278206, + 299.43967138, + 299.45599607, + 299.46307168, + 299.46457394, + 299.46522506, + 299.4653633, + 299.46542322, + 299.46543594, + ] + ), + tolerance=1e-10, + ) + assert_near_equal( + prob.get_val("phase1.vm.bcs.T", units="K"), + np.array( + [ + 300.0, + 307.73479649, + 309.37700306, + 310.08878525, + 310.23990666, + 310.30540727, + 310.31931397, + 310.32534156, + 310.3266213, + 310.32717598, + 310.32729375, + ] + ), + tolerance=1e-10, + ) # prob.model.list_outputs(print_arrays=True, units='True') - partials = prob.check_partials(method='cs',compact_print=True, step=1e-50) + partials = prob.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) diff --git a/openconcept/thermal/tests/test_chiller.py b/openconcept/thermal/tests/test_chiller.py index d311cbe0..5e429817 100644 --- a/openconcept/thermal/tests/test_chiller.py +++ b/openconcept/thermal/tests/test_chiller.py @@ -2,8 +2,14 @@ import numpy as np from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials from openmdao.api import Problem, NewtonSolver, DirectSolver -from openconcept.thermal.chiller import (LinearSelector, COPHeatPump, HeatPumpWeight, - HeatPumpWithIntegratedCoolantLoop, COPExplicit) +from openconcept.thermal.chiller import ( + LinearSelector, + COPHeatPump, + HeatPumpWeight, + HeatPumpWithIntegratedCoolantLoop, + COPExplicit, +) + class LinearSelectorTestCase(unittest.TestCase): def test_bypass(self): @@ -13,134 +19,151 @@ def test_bypass(self): p.setup(force_alloc_complex=True) p.run_model() - assert_near_equal(p.get_val('T_out_cold', units='K'), p.get_val('T_in_hot', units='K')) - assert_near_equal(p.get_val('T_out_hot', units='K'), p.get_val('T_in_cold', units='K')) - assert_near_equal(p.get_val('elec_load', units='W'), np.zeros(1)) - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal(p.get_val("T_out_cold", units="K"), p.get_val("T_in_hot", units="K")) + assert_near_equal(p.get_val("T_out_hot", units="K"), p.get_val("T_in_cold", units="K")) + assert_near_equal(p.get_val("elec_load", units="W"), np.zeros(1)) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_no_bypass(self): p = Problem() p.model.linear_solver = DirectSolver() p.model = LinearSelector() p.setup(force_alloc_complex=True) - p.set_val('bypass', np.zeros(1)) + p.set_val("bypass", np.zeros(1)) p.run_model() - assert_near_equal(p.get_val('T_out_cold', units='K'), p.get_val('T_out_refrig_cold', units='K')) - assert_near_equal(p.get_val('T_out_hot', units='K'), p.get_val('T_out_refrig_hot', units='K')) - assert_near_equal(p.get_val('elec_load', units='W'), p.get_val('elec_load', units='W')) - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal(p.get_val("T_out_cold", units="K"), p.get_val("T_out_refrig_cold", units="K")) + assert_near_equal(p.get_val("T_out_hot", units="K"), p.get_val("T_out_refrig_hot", units="K")) + assert_near_equal(p.get_val("elec_load", units="W"), p.get_val("elec_load", units="W")) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_vectorized(self): p = Problem() p.model.linear_solver = DirectSolver() p.model = LinearSelector(num_nodes=3) p.setup(force_alloc_complex=True) - p.set_val('bypass', np.array([0., 0.5, 1.])) - p.set_val('T_in_cold', np.array([295., 300., 305.]), units='K') - p.set_val('T_in_hot', np.array([295., 290., 285.]), units='K') - p.set_val('T_out_refrig_cold', np.array([[250., 260., 270.]]), units='K') - p.set_val('T_out_refrig_hot', np.array([[350., 360., 370.]]), units='K') - p.set_val('power_rating', 100., units='W') + p.set_val("bypass", np.array([0.0, 0.5, 1.0])) + p.set_val("T_in_cold", np.array([295.0, 300.0, 305.0]), units="K") + p.set_val("T_in_hot", np.array([295.0, 290.0, 285.0]), units="K") + p.set_val("T_out_refrig_cold", np.array([[250.0, 260.0, 270.0]]), units="K") + p.set_val("T_out_refrig_hot", np.array([[350.0, 360.0, 370.0]]), units="K") + p.set_val("power_rating", 100.0, units="W") p.run_model() - assert_near_equal(p.get_val('T_out_cold', units='K'), np.array([250., 275., 285.])) - assert_near_equal(p.get_val('T_out_hot', units='K'), np.array([350., 330., 305.])) - assert_near_equal(p.get_val('elec_load', units='W'), np.array([100., 50., 0.])/0.95) - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal(p.get_val("T_out_cold", units="K"), np.array([250.0, 275.0, 285.0])) + assert_near_equal(p.get_val("T_out_hot", units="K"), np.array([350.0, 330.0, 305.0])) + assert_near_equal(p.get_val("elec_load", units="W"), np.array([100.0, 50.0, 0.0]) / 0.95) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class COPHeatPumpTestCase(unittest.TestCase): def test_single(self): p = Problem() p.model.linear_solver = DirectSolver() p.model = COPHeatPump() p.setup(force_alloc_complex=True) - p.set_val('COP', np.ones(1)) - p.set_val('power_rating', 1., units='kW') + p.set_val("COP", np.ones(1)) + p.set_val("power_rating", 1.0, units="kW") p.run_model() - assert_near_equal(p.get_val('q_in_1', units='W'), np.array([-1000.])) - assert_near_equal(p.get_val('q_in_2', units='W'), np.array([2000.])) - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal(p.get_val("q_in_1", units="W"), np.array([-1000.0])) + assert_near_equal(p.get_val("q_in_2", units="W"), np.array([2000.0])) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_vectorized(self): p = Problem() p.model.linear_solver = DirectSolver() p.model = COPHeatPump(num_nodes=3) p.setup(force_alloc_complex=True) - p.set_val('COP', np.array([1., 1.5, 2.])) - p.set_val('power_rating', 1., units='kW') + p.set_val("COP", np.array([1.0, 1.5, 2.0])) + p.set_val("power_rating", 1.0, units="kW") p.run_model() - assert_near_equal(p.get_val('q_in_1', units='W'), np.array([-1000., -1500., -2000.])) - assert_near_equal(p.get_val('q_in_2', units='W'), np.array([2000., 2500., 3000.])) - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal(p.get_val("q_in_1", units="W"), np.array([-1000.0, -1500.0, -2000.0])) + assert_near_equal(p.get_val("q_in_2", units="W"), np.array([2000.0, 2500.0, 3000.0])) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class HeatPumpWeightTestCase(unittest.TestCase): def test_single(self): p = Problem() p.model.linear_solver = DirectSolver() p.model = HeatPumpWeight() p.setup(force_alloc_complex=True) - p.set_val('power_rating', 1., units='kW') - p.set_val('specific_power', 200., units='W/kg') + p.set_val("power_rating", 1.0, units="kW") + p.set_val("specific_power", 200.0, units="W/kg") p.run_model() - assert_near_equal(p.get_val('component_weight', units='kg'), np.array([5.])) - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal(p.get_val("component_weight", units="kg"), np.array([5.0])) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class HeatPumpWithIntegratedCoolantLoopTestCase(unittest.TestCase): """ Test the convergence of the HeatPumpWithIntegratedCoolantLoop Group """ + def test_no_bypass(self): # Set up the heat pump problem with 11 evaluation points nn = 4 p = Problem() p.model = HeatPumpWithIntegratedCoolantLoop(num_nodes=nn) - p.model.set_input_defaults('power_rating', val=1., units='kW') + p.model.set_input_defaults("power_rating", val=1.0, units="kW") p.model.linear_solver = DirectSolver() p.model.nonlinear_solver = NewtonSolver() - p.model.nonlinear_solver.options['solve_subsystems'] = True + p.model.nonlinear_solver.options["solve_subsystems"] = True p.setup() - p.set_val('T_in_hot', 350., units='K') - p.set_val('T_in_cold', 300., units='K') - p.set_val('mdot_coolant', 1., units='kg/s') - p.set_val('control.bypass_start', 0.) - p.set_val('control.bypass_end', 0.) + p.set_val("T_in_hot", 350.0, units="K") + p.set_val("T_in_cold", 300.0, units="K") + p.set_val("mdot_coolant", 1.0, units="kg/s") + p.set_val("control.bypass_start", 0.0) + p.set_val("control.bypass_end", 0.0) p.run_model() - assert_near_equal(p.get_val('T_out_hot', units='K'), 350.87503524*np.ones(nn), tolerance=1e-10) - assert_near_equal(p.get_val('T_out_cold', units='K'), 299.38805342*np.ones(nn), tolerance=1e-10) - assert_near_equal(p.get_val('component_weight', units='kg'), np.array([5.]), tolerance=1e-10) - assert_near_equal(p.get_val('elec_load', units='W'), 1052.63157895*np.ones(nn), tolerance=1e-10) - + assert_near_equal(p.get_val("T_out_hot", units="K"), 350.87503524 * np.ones(nn), tolerance=1e-10) + assert_near_equal(p.get_val("T_out_cold", units="K"), 299.38805342 * np.ones(nn), tolerance=1e-10) + assert_near_equal(p.get_val("component_weight", units="kg"), np.array([5.0]), tolerance=1e-10) + assert_near_equal(p.get_val("elec_load", units="W"), 1052.63157895 * np.ones(nn), tolerance=1e-10) + def test_varying_bypass(self): nn = 4 p = Problem() p.model = HeatPumpWithIntegratedCoolantLoop(num_nodes=nn) - p.model.set_input_defaults('power_rating', val=1., units='kW') + p.model.set_input_defaults("power_rating", val=1.0, units="kW") p.model.linear_solver = DirectSolver() p.model.nonlinear_solver = NewtonSolver() - p.model.nonlinear_solver.options['solve_subsystems'] = True + p.model.nonlinear_solver.options["solve_subsystems"] = True p.setup() - p.set_val('T_in_hot', 350., units='K') - p.set_val('T_in_cold', 300., units='K') - p.set_val('mdot_coolant', 1., units='kg/s') - p.set_val('control.bypass_start', 0.) - p.set_val('control.bypass_end', 1.) + p.set_val("T_in_hot", 350.0, units="K") + p.set_val("T_in_cold", 300.0, units="K") + p.set_val("mdot_coolant", 1.0, units="kg/s") + p.set_val("control.bypass_start", 0.0) + p.set_val("control.bypass_end", 1.0) p.run_model() - assert_near_equal(p.get_val('T_out_hot', units='K'), np.array([350.87503524, 333.91669016, 316.95834508, 300.]), tolerance=1e-10) - assert_near_equal(p.get_val('T_out_cold', units='K'), np.array([299.38805342, 316.25870228, 333.12935114, 350.]), tolerance=1e-10) - assert_near_equal(p.get_val('component_weight', units='kg'), np.array([5.]), tolerance=1e-10) - assert_near_equal(p.get_val('elec_load', units='W'), np.array([1052.63157895, 701.75438596, 350.87719298, 0.]), tolerance=1e-10) + assert_near_equal( + p.get_val("T_out_hot", units="K"), + np.array([350.87503524, 333.91669016, 316.95834508, 300.0]), + tolerance=1e-10, + ) + assert_near_equal( + p.get_val("T_out_cold", units="K"), + np.array([299.38805342, 316.25870228, 333.12935114, 350.0]), + tolerance=1e-10, + ) + assert_near_equal(p.get_val("component_weight", units="kg"), np.array([5.0]), tolerance=1e-10) + assert_near_equal( + p.get_val("elec_load", units="W"), + np.array([1052.63157895, 701.75438596, 350.87719298, 0.0]), + tolerance=1e-10, + ) + class COPExplicitTestCase(unittest.TestCase): def test_single(self): @@ -148,21 +171,21 @@ def test_single(self): p.model.linear_solver = DirectSolver() p.model = COPExplicit() p.setup() - p.set_val('T_c', 300.*np.ones(1), units='K') - p.set_val('T_h', 400.*np.ones(1), units='K') - p.set_val('eff_factor', 0.4) + p.set_val("T_c", 300.0 * np.ones(1), units="K") + p.set_val("T_h", 400.0 * np.ones(1), units="K") + p.set_val("eff_factor", 0.4) p.run_model() - assert_near_equal(p.get_val('COP'), np.array([1.20001629]), tolerance=1e-8) - + assert_near_equal(p.get_val("COP"), np.array([1.20001629]), tolerance=1e-8) + def test_vectorized(self): p = Problem() p.model.linear_solver = DirectSolver() p.model = COPExplicit(num_nodes=4) p.setup() - p.set_val('T_c', np.array([100., 110., 120., 130.]), units='K') - p.set_val('T_h', np.array([200., 190., 180., 170.]), units='K') - p.set_val('eff_factor', 0.4) + p.set_val("T_c", np.array([100.0, 110.0, 120.0, 130.0]), units="K") + p.set_val("T_h", np.array([200.0, 190.0, 180.0, 170.0]), units="K") + p.set_val("eff_factor", 0.4) p.run_model() - assert_near_equal(p.get_val('COP'), np.array([0.40000535, 0.5500066, 0.80000934, 1.30001871]), tolerance=1e-8) + assert_near_equal(p.get_val("COP"), np.array([0.40000535, 0.5500066, 0.80000934, 1.30001871]), tolerance=1e-8) diff --git a/openconcept/thermal/tests/test_ducts.py b/openconcept/thermal/tests/test_ducts.py index bc38c271..6a4339e6 100644 --- a/openconcept/thermal/tests/test_ducts.py +++ b/openconcept/thermal/tests/test_ducts.py @@ -2,16 +2,17 @@ import numpy as np from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials from openconcept.thermal import ImplicitCompressibleDuct_ExternalHX -import openmdao.api as om +import openmdao.api as om import warnings try: import pycycle + pyc_version = float(pycycle.__version__) if pyc_version >= 3.0: HAS_PYCYCLE = True import pycycle.api as pyc - + class PyCycleDuct(pyc.Cycle): """ This is tested with pycycle master as of 10 March 2021 @@ -19,106 +20,108 @@ class PyCycleDuct(pyc.Cycle): """ def setup(self): - design = self.options['design'] + design = self.options["design"] USE_TABULAR = False - if USE_TABULAR: - self.options['thermo_method'] = 'TABULAR' - self.options['thermo_data'] = pyc.AIR_JETA_TAB_SPEC - else: - self.options['thermo_method'] = 'CEA' - self.options['thermo_data'] = pyc.species_data.janaf - FUEL_TYPE = 'JP-7' - - self.add_subsystem('fc', pyc.FlightConditions()) + if USE_TABULAR: + self.options["thermo_method"] = "TABULAR" + self.options["thermo_data"] = pyc.AIR_JETA_TAB_SPEC + else: + self.options["thermo_method"] = "CEA" + self.options["thermo_data"] = pyc.species_data.janaf + FUEL_TYPE = "JP-7" + + self.add_subsystem("fc", pyc.FlightConditions()) # ram_recovery | ram_recovery # MN | area - self.add_subsystem('inlet', pyc.Inlet()) + self.add_subsystem("inlet", pyc.Inlet()) # dPqP | s_dPqP # Q_dot | Q_dot # MN | area - self.add_subsystem('duct', pyc.Duct()) + self.add_subsystem("duct", pyc.Duct()) # Ps_exhaust # dPqP - self.add_subsystem('nozz', pyc.Nozzle(lossCoef='Cfg')) - self.add_subsystem('perf', pyc.Performance(num_nozzles=1, num_burners=0)) - + self.add_subsystem("nozz", pyc.Nozzle(lossCoef="Cfg")) + self.add_subsystem("perf", pyc.Performance(num_nozzles=1, num_burners=0)) balance = om.BalanceComp() if design: - self.add_subsystem('iv', om.IndepVarComp('nozzle_area', 60., units='inch**2')) - balance.add_balance('W', units='kg/s', eq_units='inch**2', val=2., lower=0.05, upper=10.) - self.add_subsystem('balance', balance) - self.connect('iv.nozzle_area', 'balance.rhs:W') - self.connect('nozz.Throat:stat:area', 'balance.lhs:W') + self.add_subsystem("iv", om.IndepVarComp("nozzle_area", 60.0, units="inch**2")) + balance.add_balance("W", units="kg/s", eq_units="inch**2", val=2.0, lower=0.05, upper=10.0) + self.add_subsystem("balance", balance) + self.connect("iv.nozzle_area", "balance.rhs:W") + self.connect("nozz.Throat:stat:area", "balance.lhs:W") else: - balance.add_balance('W', units='kg/s', eq_units='inch**2', val=2., lower=0.05, upper=10.) - self.add_subsystem('balance', balance) - self.connect('nozz.Throat:stat:area', 'balance.lhs:W') - + balance.add_balance("W", units="kg/s", eq_units="inch**2", val=2.0, lower=0.05, upper=10.0) + self.add_subsystem("balance", balance) + self.connect("nozz.Throat:stat:area", "balance.lhs:W") - self.pyc_connect_flow('fc.Fl_O', 'inlet.Fl_I') - self.pyc_connect_flow('inlet.Fl_O', 'duct.Fl_I') - self.pyc_connect_flow('duct.Fl_O', 'nozz.Fl_I') + self.pyc_connect_flow("fc.Fl_O", "inlet.Fl_I") + self.pyc_connect_flow("inlet.Fl_O", "duct.Fl_I") + self.pyc_connect_flow("duct.Fl_O", "nozz.Fl_I") + self.connect("fc.Fl_O:stat:P", "nozz.Ps_exhaust") + self.connect("inlet.Fl_O:tot:P", "perf.Pt2") + self.connect("duct.Fl_O:tot:P", "perf.Pt3") + self.connect("inlet.F_ram", "perf.ram_drag") + self.connect("nozz.Fg", "perf.Fg_0") - self.connect('fc.Fl_O:stat:P', 'nozz.Ps_exhaust') - self.connect('inlet.Fl_O:tot:P', 'perf.Pt2') - self.connect('duct.Fl_O:tot:P', 'perf.Pt3') - self.connect('inlet.F_ram', 'perf.ram_drag') - self.connect('nozz.Fg', 'perf.Fg_0') - - self.connect('balance.W', 'fc.W') + self.connect("balance.W", "fc.W") newton = self.nonlinear_solver = om.NewtonSolver() - newton.options['atol'] = 1e-12 - newton.options['rtol'] = 1e-12 - newton.options['iprint'] = 2 - newton.options['maxiter'] = 10 - newton.options['solve_subsystems'] = True - newton.options['max_sub_solves'] = 10 - newton.options['reraise_child_analysiserror'] = False + newton.options["atol"] = 1e-12 + newton.options["rtol"] = 1e-12 + newton.options["iprint"] = 2 + newton.options["maxiter"] = 10 + newton.options["solve_subsystems"] = True + newton.options["max_sub_solves"] = 10 + newton.options["reraise_child_analysiserror"] = False newton.linesearch = om.BoundsEnforceLS() - newton.linesearch.options['bound_enforcement'] = 'scalar' + newton.linesearch.options["bound_enforcement"] = "scalar" # newton.linesearch.options['print_bound_enforce'] = True # newton.linesearch.options['iprint'] = -1 self.linear_solver = om.DirectSolver(assemble_jac=True) super().setup() - + def viewer(prob, pt): """ print a report of all the relevant cycle properties """ - fs_names = ['fc.Fl_O', 'inlet.Fl_O', 'duct.Fl_O', 'nozz.Fl_O'] - fs_full_names = [f'{pt}.{fs}' for fs in fs_names] + fs_names = ["fc.Fl_O", "inlet.Fl_O", "duct.Fl_O", "nozz.Fl_O"] + fs_full_names = [f"{pt}.{fs}" for fs in fs_names] pyc.print_flow_station(prob, fs_full_names) # pyc.print_compressor(prob, [f'{pt}.fan']) - pyc.print_nozzle(prob, [f'{pt}.nozz']) + pyc.print_nozzle(prob, [f"{pt}.nozz"]) - summary_data = (prob[pt+'.fc.Fl_O:stat:MN'], prob[pt+'.fc.alt'],prob[pt+'.inlet.Fl_O:stat:W'], - prob[pt+'.perf.Fn'], prob[pt+'.perf.Fg'], prob[pt+'.inlet.F_ram'], - prob[pt+'.perf.OPR']) + summary_data = ( + prob[pt + ".fc.Fl_O:stat:MN"], + prob[pt + ".fc.alt"], + prob[pt + ".inlet.Fl_O:stat:W"], + prob[pt + ".perf.Fn"], + prob[pt + ".perf.Fg"], + prob[pt + ".inlet.F_ram"], + prob[pt + ".perf.OPR"], + ) print("----------------------------------------------------------------------------") print(" POINT:", pt) print("----------------------------------------------------------------------------") print(" PERFORMANCE CHARACTERISTICS") print(" Mach Alt W Fn Fg Fram OPR ") - print(" %7.5f %7.1f %7.3f %7.1f %7.1f %7.1f %7.3f" %summary_data) - - class MPDuct(pyc.MPCycle): + print(" %7.5f %7.1f %7.3f %7.1f %7.1f %7.1f %7.3f" % summary_data) + class MPDuct(pyc.MPCycle): def setup(self): - self.options['thermo_method'] = 'CEA' - self.options['thermo_data'] = pyc.species_data.janaf + self.options["thermo_method"] = "CEA" + self.options["thermo_data"] = pyc.species_data.janaf + + design = self.pyc_add_pnt("design", PyCycleDuct(design=True, thermo_method="CEA")) - design = self.pyc_add_pnt('design', PyCycleDuct(design=True, thermo_method='CEA')) - # define the off-design conditions we want to run self.od_pts = [] # self.od_pts = ['off_design'] @@ -129,103 +132,118 @@ def setup(self): # self.pyc_add_pnt(pt, PyCycleDuct(design=False)) # self.set_input_defaults(pt+'.fc.MN', val=self.od_MNs[i]) - # self.set_input_defaults(pt+'.fc.alt', val=self.od_alts, units='m') + # self.set_input_defaults(pt+'.fc.alt', val=self.od_alts, units='m') # self.pyc_use_default_des_od_conns() # self.pyc_connect_des_od('nozz.Throat:stat:area', 'balance.rhs:W') super().setup() - - def print_truth(name, value, list_output=True): if list_output: - print(name+': '+str(value)) + print(name + ": " + str(value)) - def check_params_match_pycycle(prob, list_output=True, case_name=''): + def check_params_match_pycycle(prob, list_output=True, case_name=""): list_of_vals = [] if list_output: - print('========='+case_name+'=============') - mdot_pyc = prob.get_val('pyduct.design.fc.Fl_O:stat:W', units='kg/s') - mdot_oc = prob.get_val('oc.mdot', units='kg/s') + print("=========" + case_name + "=============") + mdot_pyc = prob.get_val("pyduct.design.fc.Fl_O:stat:W", units="kg/s") + mdot_oc = prob.get_val("oc.mdot", units="kg/s") assert_near_equal(mdot_oc, mdot_pyc, tolerance=1e-4) list_of_vals.append(mdot_pyc[0]) - print_truth('mass flow', mdot_pyc, list_output) + print_truth("mass flow", mdot_pyc, list_output) - fnet_pyc = prob.get_val('pyduct.design.perf.Fn', units='N') - fnet_oc = prob.get_val('oc.force.F_net', units='N') + fnet_pyc = prob.get_val("pyduct.design.perf.Fn", units="N") + fnet_oc = prob.get_val("oc.force.F_net", units="N") assert_near_equal(fnet_oc, fnet_pyc, tolerance=1e-4) list_of_vals.append(fnet_pyc[0]) - print_truth('net force', fnet_pyc, list_output) + print_truth("net force", fnet_pyc, list_output) # compare the flow conditions at each station - oc_stations = ['inlet','sta1','sta3','nozzle'] - state_units = ['K','Pa','kg/m**3',None,'m/s','K','Pa','inch**2'] - for i, pyc_station in enumerate(['fc','inlet', 'duct', 'nozz']): + oc_stations = ["inlet", "sta1", "sta3", "nozzle"] + state_units = ["K", "Pa", "kg/m**3", None, "m/s", "K", "Pa", "inch**2"] + for i, pyc_station in enumerate(["fc", "inlet", "duct", "nozz"]): oc_station = oc_stations[i] if list_output: - print('--------'+pyc_station+'-------------') - if oc_station == 'nozzle' or oc_station == 'inlet': - oc_states = ['T','p','rho','M','a','Tt','pt','area'] + print("--------" + pyc_station + "-------------") + if oc_station == "nozzle" or oc_station == "inlet": + oc_states = ["T", "p", "rho", "M", "a", "Tt", "pt", "area"] else: - oc_states = ['T','p','rho','M','a','Tt_out','pt_out','area'] - for j, pyc_state in enumerate(['stat:T','stat:P','stat:rho','stat:MN','stat:Vsonic','tot:T','tot:P','stat:area']): + oc_states = ["T", "p", "rho", "M", "a", "Tt_out", "pt_out", "area"] + for j, pyc_state in enumerate( + ["stat:T", "stat:P", "stat:rho", "stat:MN", "stat:Vsonic", "tot:T", "tot:P", "stat:area"] + ): oc_state = oc_states[j] - if oc_station == 'inlet' and oc_state in ['rho','area']: + if oc_station == "inlet" and oc_state in ["rho", "area"]: continue - state_pyc = prob.get_val('pyduct.design.'+pyc_station+'.Fl_O:'+pyc_state, units=state_units[j]) - state_oc = prob.get_val('oc.'+oc_station+'.'+oc_state, units=state_units[j]) + state_pyc = prob.get_val( + "pyduct.design." + pyc_station + ".Fl_O:" + pyc_state, units=state_units[j] + ) + state_oc = prob.get_val("oc." + oc_station + "." + oc_state, units=state_units[j]) assert_near_equal(state_oc, state_pyc, tolerance=5e-4) list_of_vals.append(state_pyc[0]) print_truth(oc_state, state_pyc, list_output) print(list_of_vals) + else: HAS_PYCYCLE = False - + except: HAS_PYCYCLE = False -def run_problem(ram_recovery=1.0, dPqP=0.0, heat_in=0.0, cfg=0.98, oc_use_dpqp=False, list_output=True, oc_areas=None, oc_delta_p=0.0): + +def run_problem( + ram_recovery=1.0, + dPqP=0.0, + heat_in=0.0, + cfg=0.98, + oc_use_dpqp=False, + list_output=True, + oc_areas=None, + oc_delta_p=0.0, +): prob = om.Problem() model = prob.model = om.Group() - iv = model.add_subsystem('iv', om.IndepVarComp()) - iv.add_output('area_2', val=408, units='inch**2') - + iv = model.add_subsystem("iv", om.IndepVarComp()) + iv.add_output("area_2", val=408, units="inch**2") # add the pycycle duct if HAS_PYCYCLE: - mp_duct = model.add_subsystem('pyduct', MPDuct()) - prob.model.connect('pyduct.design.fc.Fl_O:stat:T','fltcond|T') - prob.model.connect('pyduct.design.fc.Fl_O:stat:P','fltcond|p') - prob.model.connect('pyduct.design.fc.Fl_O:stat:V','fltcond|Utrue') + mp_duct = model.add_subsystem("pyduct", MPDuct()) + prob.model.connect("pyduct.design.fc.Fl_O:stat:T", "fltcond|T") + prob.model.connect("pyduct.design.fc.Fl_O:stat:P", "fltcond|p") + prob.model.connect("pyduct.design.fc.Fl_O:stat:V", "fltcond|Utrue") else: - iv.add_output('fltcond|T', val=223.15013852435118, units='K') - iv.add_output('fltcond|p', val=26436.23048846425, units='Pa') - iv.add_output('fltcond|Utrue', val=0.8*299.57996571373235, units='m/s') - prob.model.connect('iv.fltcond|T','fltcond|T') - prob.model.connect('iv.fltcond|p','fltcond|p') - prob.model.connect('iv.fltcond|Utrue','fltcond|Utrue') + iv.add_output("fltcond|T", val=223.15013852435118, units="K") + iv.add_output("fltcond|p", val=26436.23048846425, units="Pa") + iv.add_output("fltcond|Utrue", val=0.8 * 299.57996571373235, units="m/s") + prob.model.connect("iv.fltcond|T", "fltcond|T") + prob.model.connect("iv.fltcond|p", "fltcond|p") + prob.model.connect("iv.fltcond|Utrue", "fltcond|Utrue") # add the openconcept duct - oc = model.add_subsystem('oc', ImplicitCompressibleDuct_ExternalHX(num_nodes=1, cfg=cfg), - promotes_inputs=[('p_inf','fltcond|p'),('T_inf','fltcond|T'),('Utrue','fltcond|Utrue')]) + oc = model.add_subsystem( + "oc", + ImplicitCompressibleDuct_ExternalHX(num_nodes=1, cfg=cfg), + promotes_inputs=[("p_inf", "fltcond|p"), ("T_inf", "fltcond|T"), ("Utrue", "fltcond|Utrue")], + ) newton = oc.nonlinear_solver = om.NewtonSolver() - newton.options['atol'] = 1e-12 - newton.options['rtol'] = 1e-12 - newton.options['iprint'] = -1 - newton.options['maxiter'] = 10 - newton.options['solve_subsystems'] = True - newton.options['max_sub_solves'] = 10 - newton.options['reraise_child_analysiserror'] = False + newton.options["atol"] = 1e-12 + newton.options["rtol"] = 1e-12 + newton.options["iprint"] = -1 + newton.options["maxiter"] = 10 + newton.options["solve_subsystems"] = True + newton.options["max_sub_solves"] = 10 + newton.options["reraise_child_analysiserror"] = False newton.linesearch = om.BoundsEnforceLS() - newton.linesearch.options['bound_enforcement'] = 'scalar' + newton.linesearch.options["bound_enforcement"] = "scalar" oc.linear_solver = om.DirectSolver(assemble_jac=True) - prob.model.connect('iv.area_2', ['oc.area_2','oc.area_3']) + prob.model.connect("iv.area_2", ["oc.area_2", "oc.area_3"]) # iv.add_output('cp', val=1002.93, units='J/kg/K') # iv.add_output('pressure_recovery_1', val=np.ones((nn,))) @@ -236,26 +254,26 @@ def run_problem(ram_recovery=1.0, dPqP=0.0, heat_in=0.0, cfg=0.98, oc_use_dpqp=F # iv.add_output('pressure_recovery_3', val=np.ones((nn,))) prob.setup() - prob.set_val('oc.area_1', val=64, units='inch**2') - prob.set_val('oc.convergence_hack', val=0.0, units='Pa') - prob.set_val('oc.area_nozzle_in', val=60.0, units='inch**2') - prob.set_val('oc.inlet.totalpressure.eta_ram', val=ram_recovery) + prob.set_val("oc.area_1", val=64, units="inch**2") + prob.set_val("oc.convergence_hack", val=0.0, units="Pa") + prob.set_val("oc.area_nozzle_in", val=60.0, units="inch**2") + prob.set_val("oc.inlet.totalpressure.eta_ram", val=ram_recovery) if HAS_PYCYCLE: - #Define the design point - prob.set_val('pyduct.design.fc.alt', 10000, units='m') - prob.set_val('pyduct.design.fc.MN', 0.8) - prob.set_val('pyduct.design.inlet.MN', 0.6) - prob.set_val('pyduct.design.inlet.ram_recovery', ram_recovery) - prob.set_val('pyduct.design.duct.MN', 0.08) - prob.set_val('pyduct.design.duct.dPqP', dPqP) - prob.set_val('pyduct.design.duct.Q_dot', heat_in, units='kW') - prob.set_val('pyduct.design.nozz.Cfg', cfg, units=None) + # Define the design point + prob.set_val("pyduct.design.fc.alt", 10000, units="m") + prob.set_val("pyduct.design.fc.MN", 0.8) + prob.set_val("pyduct.design.inlet.MN", 0.6) + prob.set_val("pyduct.design.inlet.ram_recovery", ram_recovery) + prob.set_val("pyduct.design.duct.MN", 0.08) + prob.set_val("pyduct.design.duct.dPqP", dPqP) + prob.set_val("pyduct.design.duct.Q_dot", heat_in, units="kW") + prob.set_val("pyduct.design.nozz.Cfg", cfg, units=None) # Set initial guesses for balances - prob['pyduct.design.balance.W'] = 8. - prob.model.pyduct.design.nonlinear_solver.options['atol'] = 1e-6 - prob.model.pyduct.design.nonlinear_solver.options['rtol'] = 1e-6 + prob["pyduct.design.balance.W"] = 8.0 + prob.model.pyduct.design.nonlinear_solver.options["atol"] = 1e-6 + prob.model.pyduct.design.nonlinear_solver.options["rtol"] = 1e-6 prob.set_solver_print(level=-1) prob.set_solver_print(level=-1, depth=2) @@ -265,129 +283,318 @@ def run_problem(ram_recovery=1.0, dPqP=0.0, heat_in=0.0, cfg=0.98, oc_use_dpqp=F if HAS_PYCYCLE: # set areas based on pycycle design point - prob.set_val('oc.area_1', val=prob.get_val('pyduct.design.inlet.Fl_O:stat:area', units='inch**2'), units='inch**2') - prob.set_val('iv.area_2', val=prob.get_val('pyduct.design.duct.Fl_O:stat:area', units='inch**2'), units='inch**2') + prob.set_val( + "oc.area_1", val=prob.get_val("pyduct.design.inlet.Fl_O:stat:area", units="inch**2"), units="inch**2" + ) + prob.set_val( + "iv.area_2", val=prob.get_val("pyduct.design.duct.Fl_O:stat:area", units="inch**2"), units="inch**2" + ) else: - prob.set_val('oc.area_1', val=oc_areas[0], units='inch**2') - prob.set_val('iv.area_2', val=oc_areas[1], units='inch**2') + prob.set_val("oc.area_1", val=oc_areas[0], units="inch**2") + prob.set_val("iv.area_2", val=oc_areas[1], units="inch**2") - prob.set_val('oc.sta3.heat_in', val=heat_in, units='kW') + prob.set_val("oc.sta3.heat_in", val=heat_in, units="kW") if oc_use_dpqp: - prob.set_val('oc.sta3.pressure_recovery', val=(1-dPqP), units=None) + prob.set_val("oc.sta3.pressure_recovery", val=(1 - dPqP), units=None) else: if HAS_PYCYCLE: - delta_p = prob.get_val('pyduct.design.inlet.Fl_O:tot:P', units='Pa') - prob.get_val('pyduct.design.nozz.Fl_O:tot:P', units='Pa') + delta_p = prob.get_val("pyduct.design.inlet.Fl_O:tot:P", units="Pa") - prob.get_val( + "pyduct.design.nozz.Fl_O:tot:P", units="Pa" + ) else: delta_p = oc_delta_p - prob.set_val('oc.sta3.delta_p', -delta_p, units='Pa') + prob.set_val("oc.sta3.delta_p", -delta_p, units="Pa") prob.run_model() - + if list_output and HAS_PYCYCLE: - prob.model.list_outputs(units=True, excludes=['*chem_eq*','*props*']) + prob.model.list_outputs(units=True, excludes=["*chem_eq*", "*props*"]) # prob.model.list_outputs(includes=['*oc.*force*','*perf*','*mdot*'], units=True) - print(prob.get_val('pyduct.design.inlet.Fl_O:stat:W', units='kg/s')) - print(prob.get_val('pyduct.design.perf.Fn', units='N')) + print(prob.get_val("pyduct.design.inlet.Fl_O:stat:W", units="kg/s")) + print(prob.get_val("pyduct.design.perf.Fn", units="N")) - for pt in ['design']+mp_duct.od_pts: - print('\n', '#'*10, pt, '#'*10) - viewer(prob, 'pyduct.'+pt) + for pt in ["design"] + mp_duct.od_pts: + print("\n", "#" * 10, pt, "#" * 10) + viewer(prob, "pyduct." + pt) elif list_output: prob.model.list_outputs(units=True) return prob + def check_params_match_known(prob, known_vals): - mdot_oc = prob.get_val('oc.mdot', units='kg/s') + mdot_oc = prob.get_val("oc.mdot", units="kg/s") assert_near_equal(mdot_oc, known_vals.pop(0), tolerance=1e-4) - fnet_oc = prob.get_val('oc.force.F_net', units='N') + fnet_oc = prob.get_val("oc.force.F_net", units="N") assert_near_equal(fnet_oc, known_vals.pop(0), tolerance=1e-4) # compare the flow conditions at each station - oc_stations = ['inlet','sta1','sta3','nozzle'] - state_units = ['K','Pa','kg/m**3',None,'m/s','K','Pa','inch**2'] + oc_stations = ["inlet", "sta1", "sta3", "nozzle"] + state_units = ["K", "Pa", "kg/m**3", None, "m/s", "K", "Pa", "inch**2"] for oc_station in oc_stations: - if oc_station == 'nozzle' or oc_station == 'inlet': - oc_states = ['T','p','rho','M','a','Tt','pt','area'] + if oc_station == "nozzle" or oc_station == "inlet": + oc_states = ["T", "p", "rho", "M", "a", "Tt", "pt", "area"] else: - oc_states = ['T','p','rho','M','a','Tt_out','pt_out','area'] + oc_states = ["T", "p", "rho", "M", "a", "Tt_out", "pt_out", "area"] for j, oc_state in enumerate(oc_states): - if oc_station == 'inlet' and oc_state in ['rho','area']: + if oc_station == "inlet" and oc_state in ["rho", "area"]: continue - state_oc = prob.get_val('oc.'+oc_station+'.'+oc_state, units=state_units[j]) + state_oc = prob.get_val("oc." + oc_station + "." + oc_state, units=state_units[j]) assert_near_equal(state_oc, known_vals.pop(0), tolerance=5e-4) + if not HAS_PYCYCLE: + class TestOCDuct(unittest.TestCase): def __init__(self, *args, **kwargs): self.list_output = False - warnings.warn('pycycle >= 3.0 must be installed to run reg tests using pycycle. Using cached values') + warnings.warn("pycycle >= 3.0 must be installed to run reg tests using pycycle. Using cached values") super(TestOCDuct, self).__init__(*args, **kwargs) def test_baseline(self): - prob = run_problem(heat_in=0.0, oc_use_dpqp=False, list_output=False, oc_areas=[68.6660253970519, 419.64492400826833]) - known_vals = [3.8288293812130427, -18.352679756968048, 223.15013852435112, 26436.230488463945, 0.8, 299.57996571373224, 251.78817691084615, 40308.54640098064, - 234.83838055394062, 31597.160969709043, 0.46872831137173837, 0.6, 307.3153368247888, 251.78817691084615, 40308.54640098064, 68.6660253970519, - 251.4656086903193, 40128.37627313919, 0.5559237001577775, 0.08, 317.9885117338898, 251.78817691084618, 40308.54640098064, 419.64492400826833, - 223.1501387055282, 26436.23069828319, 0.4127096009002737, 0.7999999971456052, 299.57996583521236, 251.7881769108463, 40308.54640098064, 60.00000081736329] + prob = run_problem( + heat_in=0.0, oc_use_dpqp=False, list_output=False, oc_areas=[68.6660253970519, 419.64492400826833] + ) + known_vals = [ + 3.8288293812130427, + -18.352679756968048, + 223.15013852435112, + 26436.230488463945, + 0.8, + 299.57996571373224, + 251.78817691084615, + 40308.54640098064, + 234.83838055394062, + 31597.160969709043, + 0.46872831137173837, + 0.6, + 307.3153368247888, + 251.78817691084615, + 40308.54640098064, + 68.6660253970519, + 251.4656086903193, + 40128.37627313919, + 0.5559237001577775, + 0.08, + 317.9885117338898, + 251.78817691084618, + 40308.54640098064, + 419.64492400826833, + 223.1501387055282, + 26436.23069828319, + 0.4127096009002737, + 0.7999999971456052, + 299.57996583521236, + 251.7881769108463, + 40308.54640098064, + 60.00000081736329, + ] check_params_match_known(prob, known_vals) def test_delta_p(self): - prob = run_problem(heat_in=5.0, oc_use_dpqp=False, list_output=False, oc_areas=[63.46924960435457, 409.4433340158199], oc_delta_p=2015.4273200490352) - known_vals = [3.539056269581751, -64.14356947383646, 223.15013852435118, 26436.23048846425, 0.8, 299.57996571373235, 251.78817691084626, 40308.54640098054, - 234.83838055394045, 31597.160969708944, 0.46872831137173715, 0.6, 307.3153368247886, 251.78817691084626, 40308.54640098054, 63.46924960435457, - 252.87220799749818, 38121.95962142752, 0.5251898405386665, 0.08, 318.87459959919477, 253.19656586001645, 38293.1190809315, 409.4433340158199, - 227.71856595088528, 26436.235641634095, 0.40443001001100554, 0.7469937048327284, 302.62735666768816, 253.19656586001642, 38293.1190809315, 60.00003918869304] + prob = run_problem( + heat_in=5.0, + oc_use_dpqp=False, + list_output=False, + oc_areas=[63.46924960435457, 409.4433340158199], + oc_delta_p=2015.4273200490352, + ) + known_vals = [ + 3.539056269581751, + -64.14356947383646, + 223.15013852435118, + 26436.23048846425, + 0.8, + 299.57996571373235, + 251.78817691084626, + 40308.54640098054, + 234.83838055394045, + 31597.160969708944, + 0.46872831137173715, + 0.6, + 307.3153368247886, + 251.78817691084626, + 40308.54640098054, + 63.46924960435457, + 252.87220799749818, + 38121.95962142752, + 0.5251898405386665, + 0.08, + 318.87459959919477, + 253.19656586001645, + 38293.1190809315, + 409.4433340158199, + 227.71856595088528, + 26436.235641634095, + 0.40443001001100554, + 0.7469937048327284, + 302.62735666768816, + 253.19656586001642, + 38293.1190809315, + 60.00003918869304, + ] check_params_match_known(prob, known_vals) def test_heat_addition(self): - prob = run_problem(dPqP=0.0, heat_in=5.0, oc_use_dpqp=False, list_output=False, oc_areas=[68.48861194954175, 419.6465614913434]) - known_vals = [3.818936776878921, -15.98286892624089, 223.15013852435112, 26436.230488463945, 0.8, 299.57996571373224, 251.78817691084615, 40308.546400980646, - 234.8383805539406, 31597.16096970898, 0.4687283113717375, 0.6, 307.31533682478863, 251.78817691084615, 40308.546400980646, 68.48861194954175, - 252.76912333327795, 40128.37841113136, 0.5530568656891555, 0.08, 318.8097475436091, 253.0933500013548, 40308.546400980646, 419.6465614913434, - 224.30746290344237, 26436.23069828319, 0.4105802076038991, 0.800001851306077, 300.3549387801823, 253.0933500013548, 40308.546400980646, 60.00000076028102] + prob = run_problem( + dPqP=0.0, + heat_in=5.0, + oc_use_dpqp=False, + list_output=False, + oc_areas=[68.48861194954175, 419.6465614913434], + ) + known_vals = [ + 3.818936776878921, + -15.98286892624089, + 223.15013852435112, + 26436.230488463945, + 0.8, + 299.57996571373224, + 251.78817691084615, + 40308.546400980646, + 234.8383805539406, + 31597.16096970898, + 0.4687283113717375, + 0.6, + 307.31533682478863, + 251.78817691084615, + 40308.546400980646, + 68.48861194954175, + 252.76912333327795, + 40128.37841113136, + 0.5530568656891555, + 0.08, + 318.8097475436091, + 253.0933500013548, + 40308.546400980646, + 419.6465614913434, + 224.30746290344237, + 26436.23069828319, + 0.4105802076038991, + 0.800001851306077, + 300.3549387801823, + 253.0933500013548, + 40308.546400980646, + 60.00000076028102, + ] check_params_match_known(prob, known_vals) def test_dpqp(self): - prob = run_problem(dPqP=0.05, heat_in=5.0, oc_use_dpqp=True, list_output=False, oc_areas=[63.46924960435457, 409.4433340158199]) - known_vals = [3.539056269581751, -64.14356947383646, 223.15013852435118, 26436.23048846425, 0.8, 299.57996571373235, 251.78817691084626, 40308.54640098054, - 234.83838055394045, 31597.160969708944, 0.46872831137173715, 0.6, 307.3153368247886, 251.78817691084626, 40308.54640098054, 63.46924960435457, - 252.87220799749818, 38121.95962142752, 0.5251898405386665, 0.08, 318.87459959919477, 253.19656586001645, 38293.1190809315, 409.4433340158199, - 227.71856595088528, 26436.235641634095, 0.40443001001100554, 0.7469937048327284, 302.62735666768816, 253.19656586001642, 38293.1190809315, 60.00003918869304] + prob = run_problem( + dPqP=0.05, + heat_in=5.0, + oc_use_dpqp=True, + list_output=False, + oc_areas=[63.46924960435457, 409.4433340158199], + ) + known_vals = [ + 3.539056269581751, + -64.14356947383646, + 223.15013852435118, + 26436.23048846425, + 0.8, + 299.57996571373235, + 251.78817691084626, + 40308.54640098054, + 234.83838055394045, + 31597.160969708944, + 0.46872831137173715, + 0.6, + 307.3153368247886, + 251.78817691084626, + 40308.54640098054, + 63.46924960435457, + 252.87220799749818, + 38121.95962142752, + 0.5251898405386665, + 0.08, + 318.87459959919477, + 253.19656586001645, + 38293.1190809315, + 409.4433340158199, + 227.71856595088528, + 26436.235641634095, + 0.40443001001100554, + 0.7469937048327284, + 302.62735666768816, + 253.19656586001642, + 38293.1190809315, + 60.00003918869304, + ] check_params_match_known(prob, known_vals) def test_cfg(self): - prob = run_problem(dPqP=0.05, heat_in=5.0, oc_use_dpqp=True, cfg=0.95, list_output=False, oc_areas=[63.46924960435457, 409.4433340158199]) - known_vals = [3.539056269581751, -88.14485507224843, 223.15013852435118, 26436.23048846425, 0.8, 299.57996571373235, 251.78817691084626, 40308.54640098054, - 234.83838055394045, 31597.160969708944, 0.46872831137173715, 0.6, 307.3153368247886, 251.78817691084626, 40308.54640098054, 63.46924960435457, - 252.87220799749818, 38121.95962142752, 0.5251898405386665, 0.08, 318.87459959919477, 253.19656586001645, 38293.1190809315, 409.4433340158199, - 227.71856595088528, 26436.235641634095, 0.40443001001100554, 0.7469937048327284, 302.62735666768816, 253.19656586001642, 38293.1190809315, 60.00003918869304] + prob = run_problem( + dPqP=0.05, + heat_in=5.0, + oc_use_dpqp=True, + cfg=0.95, + list_output=False, + oc_areas=[63.46924960435457, 409.4433340158199], + ) + known_vals = [ + 3.539056269581751, + -88.14485507224843, + 223.15013852435118, + 26436.23048846425, + 0.8, + 299.57996571373235, + 251.78817691084626, + 40308.54640098054, + 234.83838055394045, + 31597.160969708944, + 0.46872831137173715, + 0.6, + 307.3153368247886, + 251.78817691084626, + 40308.54640098054, + 63.46924960435457, + 252.87220799749818, + 38121.95962142752, + 0.5251898405386665, + 0.08, + 318.87459959919477, + 253.19656586001645, + 38293.1190809315, + 409.4433340158199, + 227.71856595088528, + 26436.235641634095, + 0.40443001001100554, + 0.7469937048327284, + 302.62735666768816, + 253.19656586001642, + 38293.1190809315, + 60.00003918869304, + ] check_params_match_known(prob, known_vals) else: + class TestOCDuct(unittest.TestCase): def __init__(self, *args, **kwargs): self.list_output = False super(TestOCDuct, self).__init__(*args, **kwargs) + def test_baseline(self): prob = run_problem(dPqP=0.0, heat_in=0.0, oc_use_dpqp=False, list_output=False) - check_params_match_pycycle(prob, list_output=self.list_output, case_name='baseline') - + check_params_match_pycycle(prob, list_output=self.list_output, case_name="baseline") + def test_heat_addition(self): prob = run_problem(dPqP=0.0, heat_in=5.0, oc_use_dpqp=False, list_output=False) - check_params_match_pycycle(prob, list_output=self.list_output, case_name='heat_add') + check_params_match_pycycle(prob, list_output=self.list_output, case_name="heat_add") def test_delta_p(self): prob = run_problem(dPqP=0.05, heat_in=5.0, oc_use_dpqp=False, list_output=False) - check_params_match_pycycle(prob, list_output=self.list_output, case_name='delta_p') + check_params_match_pycycle(prob, list_output=self.list_output, case_name="delta_p") def test_dpqp(self): prob = run_problem(dPqP=0.05, heat_in=5.0, oc_use_dpqp=True, list_output=False) - check_params_match_pycycle(prob, list_output=self.list_output, case_name='dpqp') + check_params_match_pycycle(prob, list_output=self.list_output, case_name="dpqp") def test_cfg(self): prob = run_problem(dPqP=0.05, heat_in=5.0, oc_use_dpqp=True, cfg=0.95, list_output=False) - check_params_match_pycycle(prob, list_output=self.list_output, case_name='cfg') + check_params_match_pycycle(prob, list_output=self.list_output, case_name="cfg") + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/openconcept/thermal/tests/test_heat_exchanger.py b/openconcept/thermal/tests/test_heat_exchanger.py index d69b6015..83fb28ea 100644 --- a/openconcept/thermal/tests/test_heat_exchanger.py +++ b/openconcept/thermal/tests/test_heat_exchanger.py @@ -14,281 +14,295 @@ CrossFlowNTUEffectiveness, NTUEffectivenessActualHeatTransfer, OutletTemperatures, - PressureDrop + PressureDrop, ) + class OSFGeometryTestGroup(Group): """ Test the offset strip fin geometry component """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points' ) + self.options.declare("num_nodes", default=1, desc="Number of analysis points") def setup(self): - nn = self.options['num_nodes'] - - iv = self.add_subsystem('iv', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('case_thickness', val=2.0, units='mm') - iv.add_output('fin_thickness', val=0.102, units='mm') - iv.add_output('plate_thickness', val=0.2, units='mm') - iv.add_output('material_k', val=190, units='W/m/K') - iv.add_output('material_rho', val=2700, units='kg/m**3') - - iv.add_output('mdot_cold', val=np.ones(nn)*1.5, units='kg/s') - iv.add_output('rho_cold', val=np.ones(nn)*0.5, units='kg/m**3') - - iv.add_output('mdot_hot', val=0.075*np.ones(nn), units='kg/s') - iv.add_output('rho_hot', val=np.ones(nn)*1020.2, units='kg/m**3') - - iv.add_output('T_in_cold', val=np.ones(nn)*45, units='degC') - iv.add_output('T_in_hot', val=np.ones(nn)*90, units='degC') - iv.add_output('n_long_cold', val=3) - iv.add_output('n_wide_cold', val=430) - iv.add_output('n_tall', val=19) - - iv.add_output('channel_height_cold', val=14, units='mm') - iv.add_output('channel_width_cold', val=1.35, units='mm') - iv.add_output('fin_length_cold', val=6, units='mm') - iv.add_output('cp_cold', val=1005, units='J/kg/K') - iv.add_output('k_cold', val=0.02596, units='W/m/K') - iv.add_output('mu_cold', val=1.789e-5, units='kg/m/s') - - iv.add_output('channel_height_hot', val=1, units='mm') - iv.add_output('channel_width_hot', val=1, units='mm') - iv.add_output('fin_length_hot', val=6, units='mm') - iv.add_output('cp_hot', val=3801, units='J/kg/K') - iv.add_output('k_hot', val=0.405, units='W/m/K') - iv.add_output('mu_hot', val=1.68e-3, units='kg/m/s') - - - - self.add_subsystem('osfgeometry', OffsetStripFinGeometry(), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('redh', HydraulicDiameterReynoldsNumber(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('osfdata', OffsetStripFinData(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('nusselt', NusseltFromColburnJ(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('convection', ConvectiveCoefficient(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('finefficiency', FinEfficiency(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('ua', UAOverall(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('ntu', NTUMethod(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('effectiveness', CrossFlowNTUEffectiveness(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('heat', NTUEffectivenessActualHeatTransfer(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('t_out', OutletTemperatures(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('delta_p', PressureDrop(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + nn = self.options["num_nodes"] + + iv = self.add_subsystem("iv", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("case_thickness", val=2.0, units="mm") + iv.add_output("fin_thickness", val=0.102, units="mm") + iv.add_output("plate_thickness", val=0.2, units="mm") + iv.add_output("material_k", val=190, units="W/m/K") + iv.add_output("material_rho", val=2700, units="kg/m**3") + + iv.add_output("mdot_cold", val=np.ones(nn) * 1.5, units="kg/s") + iv.add_output("rho_cold", val=np.ones(nn) * 0.5, units="kg/m**3") + + iv.add_output("mdot_hot", val=0.075 * np.ones(nn), units="kg/s") + iv.add_output("rho_hot", val=np.ones(nn) * 1020.2, units="kg/m**3") + + iv.add_output("T_in_cold", val=np.ones(nn) * 45, units="degC") + iv.add_output("T_in_hot", val=np.ones(nn) * 90, units="degC") + iv.add_output("n_long_cold", val=3) + iv.add_output("n_wide_cold", val=430) + iv.add_output("n_tall", val=19) + + iv.add_output("channel_height_cold", val=14, units="mm") + iv.add_output("channel_width_cold", val=1.35, units="mm") + iv.add_output("fin_length_cold", val=6, units="mm") + iv.add_output("cp_cold", val=1005, units="J/kg/K") + iv.add_output("k_cold", val=0.02596, units="W/m/K") + iv.add_output("mu_cold", val=1.789e-5, units="kg/m/s") + + iv.add_output("channel_height_hot", val=1, units="mm") + iv.add_output("channel_width_hot", val=1, units="mm") + iv.add_output("fin_length_hot", val=6, units="mm") + iv.add_output("cp_hot", val=3801, units="J/kg/K") + iv.add_output("k_hot", val=0.405, units="W/m/K") + iv.add_output("mu_hot", val=1.68e-3, units="kg/m/s") + + self.add_subsystem("osfgeometry", OffsetStripFinGeometry(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "redh", HydraulicDiameterReynoldsNumber(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("osfdata", OffsetStripFinData(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("nusselt", NusseltFromColburnJ(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "convection", ConvectiveCoefficient(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("finefficiency", FinEfficiency(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("ua", UAOverall(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("ntu", NTUMethod(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "effectiveness", CrossFlowNTUEffectiveness(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem( + "heat", NTUEffectivenessActualHeatTransfer(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("t_out", OutletTemperatures(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("delta_p", PressureDrop(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) -class OSFGeometryTestCase(unittest.TestCase): +class OSFGeometryTestCase(unittest.TestCase): def test_default_settings(self): prob = Problem(OSFGeometryTestGroup(num_nodes=1)) - prob.setup(check=True,force_alloc_complex=True) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob['osfgeometry.dh_cold'], 0.00242316, tolerance=1e-6) - assert_near_equal(prob['heat_transfer'], 10040.9846, tolerance=1e-6 ) - assert_near_equal(prob['delta_p_cold'], -135.15338626, tolerance=1e-6 ) - assert_near_equal(prob['delta_p_hot'], -9112.282754, tolerance=1e-6 ) - assert_near_equal(prob['component_weight'], 1.147605, tolerance=1e-5 ) + assert_near_equal(prob["osfgeometry.dh_cold"], 0.00242316, tolerance=1e-6) + assert_near_equal(prob["heat_transfer"], 10040.9846, tolerance=1e-6) + assert_near_equal(prob["delta_p_cold"], -135.15338626, tolerance=1e-6) + assert_near_equal(prob["delta_p_hot"], -9112.282754, tolerance=1e-6) + assert_near_equal(prob["component_weight"], 1.147605, tolerance=1e-5) - partials = prob.check_partials(method='cs',compact_print=True, show_only_incorrect=True, step=1e-50) + partials = prob.check_partials(method="cs", compact_print=True, show_only_incorrect=True, step=1e-50) assert_check_partials(partials) def test_kayslondon_10_61(self): prob = Problem(OSFGeometryTestGroup(num_nodes=1)) - prob.setup(check=True,force_alloc_complex=True) - prob.set_val('fin_thickness', 0.004, units='inch') - prob.set_val('plate_thickness', 0.004, units='inch') - prob.set_val('fin_length_cold', 1./10., units='inch') - fin_spacing = 1 / 19.35 - 0.004 # fin pitch minus fin thickness - prob.set_val('channel_height_cold', 0.0750-0.004, units='inch') - prob.set_val('channel_width_cold', fin_spacing, units='inch') - prob.set_val('n_long_cold', 2) - prob.set_val('mdot_cold', 0.0905, units='kg/s') + prob.setup(check=True, force_alloc_complex=True) + prob.set_val("fin_thickness", 0.004, units="inch") + prob.set_val("plate_thickness", 0.004, units="inch") + prob.set_val("fin_length_cold", 1.0 / 10.0, units="inch") + fin_spacing = 1 / 19.35 - 0.004 # fin pitch minus fin thickness + prob.set_val("channel_height_cold", 0.0750 - 0.004, units="inch") + prob.set_val("channel_width_cold", fin_spacing, units="inch") + prob.set_val("n_long_cold", 2) + prob.set_val("mdot_cold", 0.0905, units="kg/s") prob.run_model() prob.model.list_outputs(units=True) # test the geometry in Kays and London 3rd Ed Pg 248, Fig 10-61 - assert_near_equal(prob['osfgeometry.dh_cold'], 1.403e-3, tolerance=1e-3) - assert_near_equal(prob['redh.Re_dh_cold'], 400., tolerance=1e-2) + assert_near_equal(prob["osfgeometry.dh_cold"], 1.403e-3, tolerance=1e-3) + assert_near_equal(prob["redh.Re_dh_cold"], 400.0, tolerance=1e-2) # data directly from Kays/London at Redh=400 - assert_near_equal(prob['osfdata.j_cold'], 0.0195, tolerance=2e-1) - assert_near_equal(prob['osfdata.f_cold'], 0.0750, tolerance=2e-1 ) + assert_near_equal(prob["osfdata.j_cold"], 0.0195, tolerance=2e-1) + assert_near_equal(prob["osfdata.f_cold"], 0.0750, tolerance=2e-1) - prob.set_val('mdot_cold', 0.0905*5, units='kg/s') + prob.set_val("mdot_cold", 0.0905 * 5, units="kg/s") prob.run_model() # data directly from Kays/London at Redh=2000 - assert_near_equal(prob['redh.Re_dh_cold'], 2000., tolerance=1e-2) - assert_near_equal(prob['osfdata.j_cold'], 0.00940, tolerance=2e-1) - assert_near_equal(prob['osfdata.f_cold'], 0.0303, tolerance=3.5e-1 ) + assert_near_equal(prob["redh.Re_dh_cold"], 2000.0, tolerance=1e-2) + assert_near_equal(prob["osfdata.j_cold"], 0.00940, tolerance=2e-1) + assert_near_equal(prob["osfdata.f_cold"], 0.0303, tolerance=3.5e-1) - assert_near_equal(prob['osfgeometry.alpha_cold'], 0.672, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.delta_cold'], 0.040, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.gamma_cold'], 0.084, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.alpha_cold"], 0.672, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.delta_cold"], 0.040, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.gamma_cold"], 0.084, tolerance=1e-2) def test_kayslondon_10_55(self): prob = Problem(OSFGeometryTestGroup(num_nodes=1)) - prob.setup(check=True,force_alloc_complex=True) - prob.set_val('fin_thickness', 0.004, units='inch') - prob.set_val('plate_thickness', 0.004, units='inch') - prob.set_val('fin_length_cold', 1./8., units='inch') - fin_spacing = 1 / 15.61 - 0.004 # fin pitch minus fin thickness - prob.set_val('channel_height_cold', 0.250-0.004, units='inch') - prob.set_val('channel_width_cold', fin_spacing, units='inch') - prob.set_val('n_long_cold', 2) - prob.set_val('mdot_cold', 0.235, units='kg/s') + prob.setup(check=True, force_alloc_complex=True) + prob.set_val("fin_thickness", 0.004, units="inch") + prob.set_val("plate_thickness", 0.004, units="inch") + prob.set_val("fin_length_cold", 1.0 / 8.0, units="inch") + fin_spacing = 1 / 15.61 - 0.004 # fin pitch minus fin thickness + prob.set_val("channel_height_cold", 0.250 - 0.004, units="inch") + prob.set_val("channel_width_cold", fin_spacing, units="inch") + prob.set_val("n_long_cold", 2) + prob.set_val("mdot_cold", 0.235, units="kg/s") prob.run_model() # test the geometry in Kays and London 3rd Ed Pg 248, Fig 10-55 - assert_near_equal(prob['osfgeometry.dh_cold'], 2.383e-3, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.dh_cold"], 2.383e-3, tolerance=1e-2) # data directly from Kays/London at Redh=400 - assert_near_equal(prob['redh.Re_dh_cold'], 400., tolerance=1e-2) - assert_near_equal(prob['osfdata.j_cold'], 0.0246, tolerance=1e-1) - assert_near_equal(prob['osfdata.f_cold'], 0.104, tolerance=1e-1) - prob.set_val('mdot_cold', 0.235*5, units='kg/s') + assert_near_equal(prob["redh.Re_dh_cold"], 400.0, tolerance=1e-2) + assert_near_equal(prob["osfdata.j_cold"], 0.0246, tolerance=1e-1) + assert_near_equal(prob["osfdata.f_cold"], 0.104, tolerance=1e-1) + prob.set_val("mdot_cold", 0.235 * 5, units="kg/s") prob.run_model() # data directly from Kays/London at Redh=2000 - assert_near_equal(prob['redh.Re_dh_cold'], 2000., tolerance=1e-2) - assert_near_equal(prob['osfdata.j_cold'], 0.0111, tolerance=1e-1) - assert_near_equal(prob['osfdata.f_cold'], 0.0420, tolerance=1e-1 ) + assert_near_equal(prob["redh.Re_dh_cold"], 2000.0, tolerance=1e-2) + assert_near_equal(prob["osfdata.j_cold"], 0.0111, tolerance=1e-1) + assert_near_equal(prob["osfdata.f_cold"], 0.0420, tolerance=1e-1) - assert_near_equal(prob['osfgeometry.alpha_cold'], 0.244, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.delta_cold'], 0.032, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.gamma_cold'], 0.067, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.alpha_cold"], 0.244, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.delta_cold"], 0.032, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.gamma_cold"], 0.067, tolerance=1e-2) def test_kayslondon_10_60(self): prob = Problem(OSFGeometryTestGroup(num_nodes=1)) - prob.setup(check=True,force_alloc_complex=True) - prob.set_val('fin_thickness', 0.004, units='inch') - prob.set_val('plate_thickness', 0.004, units='inch') - prob.set_val('fin_length_cold', 1./10., units='inch') - fin_spacing = 1 / 27.03 - 0.004 # fin pitch minus fin thickness - prob.set_val('channel_height_cold', 0.250-0.004, units='inch') - prob.set_val('channel_width_cold', fin_spacing, units='inch') - prob.set_val('n_long_cold', 2) - prob.set_val('mdot_cold', 0.27, units='kg/s') + prob.setup(check=True, force_alloc_complex=True) + prob.set_val("fin_thickness", 0.004, units="inch") + prob.set_val("plate_thickness", 0.004, units="inch") + prob.set_val("fin_length_cold", 1.0 / 10.0, units="inch") + fin_spacing = 1 / 27.03 - 0.004 # fin pitch minus fin thickness + prob.set_val("channel_height_cold", 0.250 - 0.004, units="inch") + prob.set_val("channel_width_cold", fin_spacing, units="inch") + prob.set_val("n_long_cold", 2) + prob.set_val("mdot_cold", 0.27, units="kg/s") prob.run_model() # test the geometry in Kays and London 3rd Ed Pg 248, Fig 10-55 # assert_near_equal(prob['osfgeometry.dh_cold'], 0.00147796, tolerance=1e-4) - assert_near_equal(prob['osfgeometry.dh_cold'], 0.001423, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.dh_cold"], 0.001423, tolerance=1e-2) # data directly from Kays/London at Redh=500 - assert_near_equal(prob['redh.Re_dh_cold'], 500., tolerance=1e-2) - assert_near_equal(prob['osfdata.j_cold'], 0.0238, tolerance=1e-1) - assert_near_equal(prob['osfdata.f_cold'], 0.0922, tolerance=1e-1) - prob.set_val('mdot_cold', 0.27*4, units='kg/s') + assert_near_equal(prob["redh.Re_dh_cold"], 500.0, tolerance=1e-2) + assert_near_equal(prob["osfdata.j_cold"], 0.0238, tolerance=1e-1) + assert_near_equal(prob["osfdata.f_cold"], 0.0922, tolerance=1e-1) + prob.set_val("mdot_cold", 0.27 * 4, units="kg/s") prob.run_model() # data directly from Kays/London at Redh=2000 - assert_near_equal(prob['redh.Re_dh_cold'], 2000., tolerance=1e-2) - assert_near_equal(prob['osfdata.j_cold'], 0.0113, tolerance=1e-1) - assert_near_equal(prob['osfdata.f_cold'], 0.0449, tolerance=1e-1 ) - - assert_near_equal(prob['osfgeometry.alpha_cold'], 0.134, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.delta_cold'], 0.040, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.gamma_cold'], 0.121, tolerance=1e-2) - + assert_near_equal(prob["redh.Re_dh_cold"], 2000.0, tolerance=1e-2) + assert_near_equal(prob["osfdata.j_cold"], 0.0113, tolerance=1e-1) + assert_near_equal(prob["osfdata.f_cold"], 0.0449, tolerance=1e-1) + assert_near_equal(prob["osfgeometry.alpha_cold"], 0.134, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.delta_cold"], 0.040, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.gamma_cold"], 0.121, tolerance=1e-2) def test_kayslondon_10_63(self): prob = Problem(OSFGeometryTestGroup(num_nodes=1)) - prob.setup(check=True,force_alloc_complex=True) - prob.set_val('fin_thickness', 0.004, units='inch') - prob.set_val('plate_thickness', 0.004, units='inch') - prob.set_val('fin_length_cold', 3./32., units='inch') - fin_spacing = 0.082 - 0.004 # fin pitch minus fin thickness - prob.set_val('channel_height_cold', 0.485-0.004, units='inch') - prob.set_val('channel_width_cold', fin_spacing, units='inch') - prob.set_val('n_long_cold', 4) - prob.set_val('mdot_cold', 0.54, units='kg/s') + prob.setup(check=True, force_alloc_complex=True) + prob.set_val("fin_thickness", 0.004, units="inch") + prob.set_val("plate_thickness", 0.004, units="inch") + prob.set_val("fin_length_cold", 3.0 / 32.0, units="inch") + fin_spacing = 0.082 - 0.004 # fin pitch minus fin thickness + prob.set_val("channel_height_cold", 0.485 - 0.004, units="inch") + prob.set_val("channel_width_cold", fin_spacing, units="inch") + prob.set_val("n_long_cold", 4) + prob.set_val("mdot_cold", 0.54, units="kg/s") prob.run_model() # test the geometry in Kays and London 3rd Ed Pg 248, Fig 10-55 # assert_near_equal(prob['osfgeometry.dh_cold'], 0.00341, tolerance=1e-2) # data directly from Kays/London at Redh=500 - assert_near_equal(prob['redh.Re_dh_cold'], 500., tolerance=1e-2) - assert_near_equal(prob['osfdata.j_cold'], 0.0205, tolerance=2e-1) - assert_near_equal(prob['osfdata.f_cold'], 0.130, tolerance=2e-1) - prob.set_val('mdot_cold', 0.54*4, units='kg/s') + assert_near_equal(prob["redh.Re_dh_cold"], 500.0, tolerance=1e-2) + assert_near_equal(prob["osfdata.j_cold"], 0.0205, tolerance=2e-1) + assert_near_equal(prob["osfdata.f_cold"], 0.130, tolerance=2e-1) + prob.set_val("mdot_cold", 0.54 * 4, units="kg/s") prob.run_model() # data directly from Kays/London at Redh=2000 - assert_near_equal(prob['redh.Re_dh_cold'], 2000., tolerance=1e-2) - assert_near_equal(prob['osfdata.j_cold'], 0.0119, tolerance=2e-1) - assert_near_equal(prob['osfdata.f_cold'], 0.0607, tolerance=2e-1 ) + assert_near_equal(prob["redh.Re_dh_cold"], 2000.0, tolerance=1e-2) + assert_near_equal(prob["osfdata.j_cold"], 0.0119, tolerance=2e-1) + assert_near_equal(prob["osfdata.f_cold"], 0.0607, tolerance=2e-1) + + assert_near_equal(prob["osfgeometry.alpha_cold"], 0.162, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.delta_cold"], 0.043, tolerance=1e-2) + assert_near_equal(prob["osfgeometry.gamma_cold"], 0.051, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.alpha_cold'], 0.162, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.delta_cold'], 0.043, tolerance=1e-2) - assert_near_equal(prob['osfgeometry.gamma_cold'], 0.051, tolerance=1e-2) class OSFManualCheckTestGroup(Group): """ Test the offset strip fin geometry component """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points' ) + self.options.declare("num_nodes", default=1, desc="Number of analysis points") def setup(self): - nn = self.options['num_nodes'] - - iv = self.add_subsystem('iv', IndepVarComp(), promotes_outputs=['*']) - iv.add_output('case_thickness', val=2.0, units='mm') - iv.add_output('fin_thickness', val=0.1, units='mm') - iv.add_output('plate_thickness', val=0.2, units='mm') - iv.add_output('material_k', val=190, units='W/m/K') - iv.add_output('material_rho', val=2700, units='kg/m**3') - - iv.add_output('mdot_cold', val=np.ones(nn)*0.1, units='kg/s') - iv.add_output('rho_cold', val=np.ones(nn)*0.5, units='kg/m**3') - - iv.add_output('mdot_hot', val=np.ones(nn)*0.2, units='kg/s') - iv.add_output('rho_hot', val=np.ones(nn)*0.6, units='kg/m**3') - - iv.add_output('T_in_cold', val=np.ones(nn)*45, units='degC') - iv.add_output('T_in_hot', val=np.ones(nn)*90, units='degC') - iv.add_output('n_long_cold', val=25) - iv.add_output('n_wide_cold', val=25) - iv.add_output('n_tall', val=8) - - iv.add_output('channel_height_cold', val=6.0, units='mm') - iv.add_output('channel_width_cold', val=1.5, units='mm') - iv.add_output('fin_length_cold', val=3, units='mm') - iv.add_output('cp_cold', val=1005, units='J/kg/K') - iv.add_output('k_cold', val=0.02596, units='W/m/K') - iv.add_output('mu_cold', val=1.789e-5, units='kg/m/s') - - iv.add_output('channel_height_hot', val=8.0, units='mm') - iv.add_output('channel_width_hot', val=1.7, units='mm') - iv.add_output('fin_length_hot', val=3.1, units='mm') - iv.add_output('cp_hot', val=900, units='J/kg/K') - iv.add_output('k_hot', val=0.024, units='W/m/K') - iv.add_output('mu_hot', val=1.7e-5, units='kg/m/s') - - - - self.add_subsystem('osfgeometry', OffsetStripFinGeometry(), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('redh', HydraulicDiameterReynoldsNumber(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('osfdata', OffsetStripFinData(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('nusselt', NusseltFromColburnJ(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('convection', ConvectiveCoefficient(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('finefficiency', FinEfficiency(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('ua', UAOverall(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('ntu', NTUMethod(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('effectiveness', CrossFlowNTUEffectiveness(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('heat', NTUEffectivenessActualHeatTransfer(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('t_out', OutletTemperatures(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) - self.add_subsystem('delta_p', PressureDrop(num_nodes=nn), promotes_inputs=['*'], promotes_outputs=['*']) + nn = self.options["num_nodes"] + + iv = self.add_subsystem("iv", IndepVarComp(), promotes_outputs=["*"]) + iv.add_output("case_thickness", val=2.0, units="mm") + iv.add_output("fin_thickness", val=0.1, units="mm") + iv.add_output("plate_thickness", val=0.2, units="mm") + iv.add_output("material_k", val=190, units="W/m/K") + iv.add_output("material_rho", val=2700, units="kg/m**3") + + iv.add_output("mdot_cold", val=np.ones(nn) * 0.1, units="kg/s") + iv.add_output("rho_cold", val=np.ones(nn) * 0.5, units="kg/m**3") + + iv.add_output("mdot_hot", val=np.ones(nn) * 0.2, units="kg/s") + iv.add_output("rho_hot", val=np.ones(nn) * 0.6, units="kg/m**3") + + iv.add_output("T_in_cold", val=np.ones(nn) * 45, units="degC") + iv.add_output("T_in_hot", val=np.ones(nn) * 90, units="degC") + iv.add_output("n_long_cold", val=25) + iv.add_output("n_wide_cold", val=25) + iv.add_output("n_tall", val=8) + + iv.add_output("channel_height_cold", val=6.0, units="mm") + iv.add_output("channel_width_cold", val=1.5, units="mm") + iv.add_output("fin_length_cold", val=3, units="mm") + iv.add_output("cp_cold", val=1005, units="J/kg/K") + iv.add_output("k_cold", val=0.02596, units="W/m/K") + iv.add_output("mu_cold", val=1.789e-5, units="kg/m/s") + + iv.add_output("channel_height_hot", val=8.0, units="mm") + iv.add_output("channel_width_hot", val=1.7, units="mm") + iv.add_output("fin_length_hot", val=3.1, units="mm") + iv.add_output("cp_hot", val=900, units="J/kg/K") + iv.add_output("k_hot", val=0.024, units="W/m/K") + iv.add_output("mu_hot", val=1.7e-5, units="kg/m/s") + + self.add_subsystem("osfgeometry", OffsetStripFinGeometry(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "redh", HydraulicDiameterReynoldsNumber(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("osfdata", OffsetStripFinData(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("nusselt", NusseltFromColburnJ(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "convection", ConvectiveCoefficient(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("finefficiency", FinEfficiency(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("ua", UAOverall(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("ntu", NTUMethod(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "effectiveness", CrossFlowNTUEffectiveness(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem( + "heat", NTUEffectivenessActualHeatTransfer(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("t_out", OutletTemperatures(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("delta_p", PressureDrop(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) -class TestHXByHand(unittest.TestCase): +class TestHXByHand(unittest.TestCase): def test_by_hand(self): """ This test case verifies that the implementation of the equations from Kays and London are correct - and not mistyped. + and not mistyped. """ prob = Problem(OSFManualCheckTestGroup(num_nodes=1)) - prob.setup(check=True,force_alloc_complex=True) + prob.setup(check=True, force_alloc_complex=True) prob.run_model() h_c = 6.0 h_h = 8.0 - w_c = 1.5 + w_c = 1.5 w_h = 1.7 l_f_c = 3.0 l_f_h = 3.1 @@ -303,144 +317,177 @@ def test_by_hand(self): hot_one_layer_height = t_p + t_f + h_h hot_one_cell_width = t_f + w_h n_hot_wide = cold_length / hot_one_cell_width - + #######____ - #| | | - #| | | - #| | | - #|____| |___ + # | | | + # | | | + # | | | + # |____| |___ ########## height_overall = n_cold_tall * (cold_one_layer_height + hot_one_layer_height) # note does not include case - assert_near_equal(prob['osfgeometry.height_overall'], height_overall / 1000, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.height_overall"], height_overall / 1000, tolerance=1e-6) width_overall = n_cold_wide * cold_one_cell_width - assert_near_equal(prob['osfgeometry.width_overall'], width_overall / 1000, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.width_overall"], width_overall / 1000, tolerance=1e-6) length_overall = n_cold_long * l_f_c - assert_near_equal(prob['osfgeometry.length_overall'], length_overall / 1000, tolerance=1e-6) - frontal_area = width_overall*height_overall - assert_near_equal(prob['osfgeometry.frontal_area'], frontal_area / 1000 ** 2, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.length_overall"], length_overall / 1000, tolerance=1e-6) + frontal_area = width_overall * height_overall + assert_near_equal(prob["osfgeometry.frontal_area"], frontal_area / 1000**2, tolerance=1e-6) xs_area_cold = n_cold_wide * n_cold_tall * w_c * h_c - assert_near_equal(prob['osfgeometry.xs_area_cold'], xs_area_cold / 1000 ** 2, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.xs_area_cold"], xs_area_cold / 1000**2, tolerance=1e-6) heat_transfer_area_cold = 2 * (w_c + h_c) * n_cold_tall * n_cold_wide * length_overall - assert_near_equal(prob['osfgeometry.heat_transfer_area_cold'], heat_transfer_area_cold / 1000 ** 2, tolerance=1e-6) + assert_near_equal( + prob["osfgeometry.heat_transfer_area_cold"], heat_transfer_area_cold / 1000**2, tolerance=1e-6 + ) dh_cold = 4 * w_c * h_c * l_f_c / (2 * (w_c * l_f_c + h_c * l_f_c + t_f * h_c) + t_f * w_c) - assert_near_equal(prob['osfgeometry.dh_cold'], dh_cold / 1000, tolerance=1e-6) - assert_near_equal(prob['osfgeometry.dh_cold'], 2*h_c*w_c/(h_c + w_c) / 1000, tolerance=3e-2) + assert_near_equal(prob["osfgeometry.dh_cold"], dh_cold / 1000, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.dh_cold"], 2 * h_c * w_c / (h_c + w_c) / 1000, tolerance=3e-2) fin_area_ratio_cold = h_c / (h_c + w_c) - assert_near_equal(prob['osfgeometry.fin_area_ratio_cold'], fin_area_ratio_cold, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.fin_area_ratio_cold"], fin_area_ratio_cold, tolerance=1e-6) contraction_ratio_cold = xs_area_cold / frontal_area - assert_near_equal(prob['osfgeometry.contraction_ratio_cold'], contraction_ratio_cold, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.contraction_ratio_cold"], contraction_ratio_cold, tolerance=1e-6) alpha_cold = w_c / h_c delta_cold = t_f / l_f_c gamma_cold = t_f / w_c - assert_near_equal(prob['osfgeometry.alpha_cold'], alpha_cold, tolerance=1e-6) - assert_near_equal(prob['osfgeometry.delta_cold'], delta_cold, tolerance=1e-6) - assert_near_equal(prob['osfgeometry.gamma_cold'], gamma_cold, tolerance=1e-6) - + assert_near_equal(prob["osfgeometry.alpha_cold"], alpha_cold, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.delta_cold"], delta_cold, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.gamma_cold"], gamma_cold, tolerance=1e-6) n_hot_wide = length_overall / hot_one_cell_width xs_area_hot = n_hot_wide * n_cold_tall * w_h * h_h - assert_near_equal(prob['osfgeometry.xs_area_hot'], xs_area_hot / 1000 ** 2, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.xs_area_hot"], xs_area_hot / 1000**2, tolerance=1e-6) heat_transfer_area_hot = 2 * (w_h + h_h) * n_cold_tall * n_hot_wide * width_overall - assert_near_equal(prob['osfgeometry.heat_transfer_area_hot'], heat_transfer_area_hot / 1000 ** 2, tolerance=1e-6) + assert_near_equal( + prob["osfgeometry.heat_transfer_area_hot"], heat_transfer_area_hot / 1000**2, tolerance=1e-6 + ) dh_hot = 2 * w_h * h_h / (w_h + h_h) - assert_near_equal(prob['osfgeometry.dh_hot'], dh_hot / 1000, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.dh_hot"], dh_hot / 1000, tolerance=1e-6) fin_area_ratio_hot = h_h / (h_h + w_h) - assert_near_equal(prob['osfgeometry.fin_area_ratio_hot'], fin_area_ratio_hot, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.fin_area_ratio_hot"], fin_area_ratio_hot, tolerance=1e-6) contraction_ratio_hot = xs_area_hot / length_overall / height_overall - assert_near_equal(prob['osfgeometry.contraction_ratio_hot'], contraction_ratio_hot, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.contraction_ratio_hot"], contraction_ratio_hot, tolerance=1e-6) alpha_hot = w_h / h_h delta_hot = t_f / l_f_h gamma_hot = t_f / w_h - assert_near_equal(prob['osfgeometry.alpha_hot'], alpha_hot, tolerance=1e-6) - assert_near_equal(prob['osfgeometry.delta_hot'], delta_hot, tolerance=1e-6) - assert_near_equal(prob['osfgeometry.gamma_hot'], gamma_hot, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.alpha_hot"], alpha_hot, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.delta_hot"], delta_hot, tolerance=1e-6) + assert_near_equal(prob["osfgeometry.gamma_hot"], gamma_hot, tolerance=1e-6) mdot_cold = 0.1 mdot_hot = 0.2 - mu_cold=1.789e-5 - mu_hot=1.7e-5 + mu_cold = 1.789e-5 + mu_hot = 1.7e-5 - redh_cold = mdot_cold / (xs_area_cold / 1000 **2) * dh_cold / 1000 / mu_cold - redh_hot = mdot_hot / (xs_area_hot / 1000 ** 2) * dh_hot / 1000 / mu_hot - assert_near_equal(prob['redh.Re_dh_cold'], redh_cold, tolerance=1e-6) - assert_near_equal(prob['redh.Re_dh_hot'], redh_hot, tolerance=1e-6) + redh_cold = mdot_cold / (xs_area_cold / 1000**2) * dh_cold / 1000 / mu_cold + redh_hot = mdot_hot / (xs_area_hot / 1000**2) * dh_hot / 1000 / mu_hot + assert_near_equal(prob["redh.Re_dh_cold"], redh_cold, tolerance=1e-6) + assert_near_equal(prob["redh.Re_dh_hot"], redh_hot, tolerance=1e-6) - partials = prob.check_partials(method='cs',compact_print=True, show_only_incorrect=True, step=1e-50) + partials = prob.check_partials(method="cs", compact_print=True, show_only_incorrect=True, step=1e-50) assert_check_partials(partials) - j_cold = 0.6522*redh_cold**-0.5403*alpha_cold**-0.1541*delta_cold**0.1499*gamma_cold**-0.0678 * (1 + 5.269e-5*redh_cold**1.340*alpha_cold**0.504*delta_cold**0.456*gamma_cold**-1.055)**0.1 - j_hot = 0.6522*redh_hot**-0.5403*alpha_hot**-0.1541*delta_hot**0.1499*gamma_hot**-0.0678 * (1 + 5.269e-5*redh_hot**1.340*alpha_hot**0.504*delta_hot**0.456*gamma_hot**-1.055)**0.1 - assert_near_equal(prob['osfdata.j_hot'], j_hot, tolerance=1e-6) - assert_near_equal(prob['osfdata.j_cold'], j_cold, tolerance=1e-6) - f_cold = 9.6243*redh_cold**-0.7422*alpha_cold**-0.1856*delta_cold**0.3053*gamma_cold**-0.2659*(1+7.669e-8*redh_cold**4.429*alpha_cold**0.920*delta_cold**3.767*gamma_cold**0.236)**0.1 - f_hot = 9.6243*redh_hot**-0.7422*alpha_hot**-0.1856*delta_hot**0.3053*gamma_hot**-0.2659*(1+7.669e-8*redh_hot**4.429*alpha_hot**0.920*delta_hot**3.767*gamma_hot**0.236)**0.1 - assert_near_equal(prob['osfdata.f_hot'], f_hot, tolerance=1e-6) - assert_near_equal(prob['osfdata.f_cold'], f_cold, tolerance=1e-6) - + j_cold = ( + 0.6522 + * redh_cold**-0.5403 + * alpha_cold**-0.1541 + * delta_cold**0.1499 + * gamma_cold**-0.0678 + * (1 + 5.269e-5 * redh_cold**1.340 * alpha_cold**0.504 * delta_cold**0.456 * gamma_cold**-1.055) + ** 0.1 + ) + j_hot = ( + 0.6522 + * redh_hot**-0.5403 + * alpha_hot**-0.1541 + * delta_hot**0.1499 + * gamma_hot**-0.0678 + * (1 + 5.269e-5 * redh_hot**1.340 * alpha_hot**0.504 * delta_hot**0.456 * gamma_hot**-1.055) ** 0.1 + ) + assert_near_equal(prob["osfdata.j_hot"], j_hot, tolerance=1e-6) + assert_near_equal(prob["osfdata.j_cold"], j_cold, tolerance=1e-6) + f_cold = ( + 9.6243 + * redh_cold**-0.7422 + * alpha_cold**-0.1856 + * delta_cold**0.3053 + * gamma_cold**-0.2659 + * (1 + 7.669e-8 * redh_cold**4.429 * alpha_cold**0.920 * delta_cold**3.767 * gamma_cold**0.236) + ** 0.1 + ) + f_hot = ( + 9.6243 + * redh_hot**-0.7422 + * alpha_hot**-0.1856 + * delta_hot**0.3053 + * gamma_hot**-0.2659 + * (1 + 7.669e-8 * redh_hot**4.429 * alpha_hot**0.920 * delta_hot**3.767 * gamma_hot**0.236) ** 0.1 + ) + assert_near_equal(prob["osfdata.f_hot"], f_hot, tolerance=1e-6) + assert_near_equal(prob["osfdata.f_cold"], f_cold, tolerance=1e-6) cp_cold = 1005 k_cold = 0.02596 cp_hot = 900 k_hot = 0.024 - h_cold = j_cold * cp_cold ** (1/3) * (k_cold/mu_cold)**(2/3) * mdot_cold / xs_area_cold * 1000**2 - h_hot = j_hot * cp_hot ** (1/3) * (k_hot/mu_hot)**(2/3) * mdot_hot / xs_area_hot * 1000**2 + h_cold = j_cold * cp_cold ** (1 / 3) * (k_cold / mu_cold) ** (2 / 3) * mdot_cold / xs_area_cold * 1000**2 + h_hot = j_hot * cp_hot ** (1 / 3) * (k_hot / mu_hot) ** (2 / 3) * mdot_hot / xs_area_hot * 1000**2 - assert_near_equal(prob['convection.h_conv_cold'], h_cold, tolerance=1e-6) - assert_near_equal(prob['convection.h_conv_hot'], h_hot, tolerance=1e-6) + assert_near_equal(prob["convection.h_conv_cold"], h_cold, tolerance=1e-6) + assert_near_equal(prob["convection.h_conv_hot"], h_hot, tolerance=1e-6) k_alu = 190 # TODO kays and london has a different expression for fin efficiency. why # m = np.sqrt(2*h_cold/k_alu/(t_f/1000))*(1 + t_f / l_f_c) - m = np.sqrt(2*h_cold/k_alu/(t_f/1000)) - eta_f_cold = np.tanh(m*h_c/2/1000)/m/(h_c/2/1000) - eta_o_cold = 1 - (1-eta_f_cold)*fin_area_ratio_cold - assert_near_equal(prob['finefficiency.eta_overall_cold'], eta_o_cold, tolerance=1e-6) - m = np.sqrt(2*h_hot/k_alu/(t_f/1000)) - eta_f_hot = np.tanh(m*h_h/2/1000)/m/(h_h/2/1000) - eta_o_hot = 1 - (1-eta_f_hot)*fin_area_ratio_hot - assert_near_equal(prob['finefficiency.eta_overall_hot'], eta_o_hot, tolerance=1e-6) - - rc = 1/eta_o_cold/(heat_transfer_area_cold / 1000**2)/h_cold - rh = 1/eta_o_hot/(heat_transfer_area_hot / 1000**2)/h_hot + m = np.sqrt(2 * h_cold / k_alu / (t_f / 1000)) + eta_f_cold = np.tanh(m * h_c / 2 / 1000) / m / (h_c / 2 / 1000) + eta_o_cold = 1 - (1 - eta_f_cold) * fin_area_ratio_cold + assert_near_equal(prob["finefficiency.eta_overall_cold"], eta_o_cold, tolerance=1e-6) + m = np.sqrt(2 * h_hot / k_alu / (t_f / 1000)) + eta_f_hot = np.tanh(m * h_h / 2 / 1000) / m / (h_h / 2 / 1000) + eta_o_hot = 1 - (1 - eta_f_hot) * fin_area_ratio_hot + assert_near_equal(prob["finefficiency.eta_overall_hot"], eta_o_hot, tolerance=1e-6) + + rc = 1 / eta_o_cold / (heat_transfer_area_cold / 1000**2) / h_cold + rh = 1 / eta_o_hot / (heat_transfer_area_hot / 1000**2) / h_hot # rw = (t_f + t_p)/1000/k_alu/(length_overall * width_overall / 1000 ** 2)/n_cold_tall # TODO wall resistance not currently accounted for, less than 1% effect rw = 0.0 - uaoverall = 1 / (rc+rh+rw) - assert_near_equal(prob['ua.UA_overall'], uaoverall, tolerance=1e-6) + uaoverall = 1 / (rc + rh + rw) + assert_near_equal(prob["ua.UA_overall"], uaoverall, tolerance=1e-6) cmin = np.minimum(mdot_cold * cp_cold, mdot_hot * cp_hot) cmax = np.maximum(mdot_cold * cp_cold, mdot_hot * cp_hot) - cratio = cmin/cmax + cratio = cmin / cmax ntu = uaoverall / cmin - effectiveness = 1 - np.exp(ntu**0.22*(np.exp(-cratio*ntu**0.78)-1)/cratio) - assert_near_equal(prob['ntu.NTU'], ntu, tolerance=1e-6) - assert_near_equal(prob['effectiveness.effectiveness'], effectiveness, tolerance=1e-6) + effectiveness = 1 - np.exp(ntu**0.22 * (np.exp(-cratio * ntu**0.78) - 1) / cratio) + assert_near_equal(prob["ntu.NTU"], ntu, tolerance=1e-6) + assert_near_equal(prob["effectiveness.effectiveness"], effectiveness, tolerance=1e-6) heat_transfer = cmin * effectiveness * (90 - 45) - assert_near_equal(prob['heat.heat_transfer'], heat_transfer, tolerance=1e-6) + assert_near_equal(prob["heat.heat_transfer"], heat_transfer, tolerance=1e-6) - tout_cold = 45 + heat_transfer / mdot_cold / cp_cold +273.15 - tout_hot = 90 - heat_transfer / mdot_hot / cp_hot +273.15 - assert_near_equal(prob['t_out.T_out_cold'], tout_cold, tolerance=1e-6) - assert_near_equal(prob['t_out.T_out_hot'], tout_hot, tolerance=1e-6) + tout_cold = 45 + heat_transfer / mdot_cold / cp_cold + 273.15 + tout_hot = 90 - heat_transfer / mdot_hot / cp_hot + 273.15 + assert_near_equal(prob["t_out.T_out_cold"], tout_cold, tolerance=1e-6) + assert_near_equal(prob["t_out.T_out_hot"], tout_hot, tolerance=1e-6) - Gcold = mdot_cold / (xs_area_cold / 1000 **2) - Ghot = mdot_hot / (xs_area_hot / 1000 ** 2) + Gcold = mdot_cold / (xs_area_cold / 1000**2) + Ghot = mdot_hot / (xs_area_hot / 1000**2) rho_cold = 0.5 rho_hot = 0.6 Kc = 0.3 Ke = -0.1 - pressure_drop_cold = -Gcold ** 2 / 2 / rho_cold * ((Kc + Ke) + f_cold * 4 * length_overall / dh_cold) - pressure_drop_hot = -Ghot ** 2 / 2 / rho_hot * ((Kc + Ke) + f_hot * 4 * width_overall / dh_hot) - assert_near_equal(prob['delta_p.delta_p_cold'], pressure_drop_cold, tolerance=1e-6) - assert_near_equal(prob['delta_p.delta_p_hot'], pressure_drop_hot, tolerance=1e-6) + pressure_drop_cold = -(Gcold**2) / 2 / rho_cold * ((Kc + Ke) + f_cold * 4 * length_overall / dh_cold) + pressure_drop_hot = -(Ghot**2) / 2 / rho_hot * ((Kc + Ke) + f_hot * 4 * width_overall / dh_hot) + assert_near_equal(prob["delta_p.delta_p_cold"], pressure_drop_cold, tolerance=1e-6) + assert_near_equal(prob["delta_p.delta_p_hot"], pressure_drop_hot, tolerance=1e-6) + -if __name__=="__main__": - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/openconcept/thermal/tests/test_heat_pipe.py b/openconcept/thermal/tests/test_heat_pipe.py index c2131ac9..e16c7829 100644 --- a/openconcept/thermal/tests/test_heat_pipe.py +++ b/openconcept/thermal/tests/test_heat_pipe.py @@ -12,208 +12,241 @@ QMaxAnalyticalPart, ) + class HeatPipeIntegrationTestCase(unittest.TestCase): """ Test the HeatPipe group with everything integrated """ + def test_simple_scalar(self): nn = 1 - theta = 84. + theta = 84.0 prob = Problem() - pipe = prob.model.add_subsystem('test', HeatPipe(num_nodes=nn, theta=theta), promotes=['*']) - pipe.set_input_defaults('T_evap', units='degC', val=np.linspace(30, 30, nn)) - pipe.set_input_defaults('q', units='W', val=np.linspace(400, 400, nn)) - pipe.set_input_defaults('length', units='m', val=10.22) - pipe.set_input_defaults('inner_diam', units='inch', val=.902) - pipe.set_input_defaults('n_pipes', val=1.) - pipe.set_input_defaults('T_design', units='degC', val=40) + pipe = prob.model.add_subsystem("test", HeatPipe(num_nodes=nn, theta=theta), promotes=["*"]) + pipe.set_input_defaults("T_evap", units="degC", val=np.linspace(30, 30, nn)) + pipe.set_input_defaults("q", units="W", val=np.linspace(400, 400, nn)) + pipe.set_input_defaults("length", units="m", val=10.22) + pipe.set_input_defaults("inner_diam", units="inch", val=0.902) + pipe.set_input_defaults("n_pipes", val=1.0) + pipe.set_input_defaults("T_design", units="degC", val=40) prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob['q_max'], np.ones(nn)*2807.01928115, tolerance=1e-5) - assert_near_equal(prob['weight'], 0.51463886, tolerance=1e-5) - assert_near_equal(prob['T_cond'], np.ones(nn)*29.9441105, tolerance=1e-5) + assert_near_equal(prob["q_max"], np.ones(nn) * 2807.01928115, tolerance=1e-5) + assert_near_equal(prob["weight"], 0.51463886, tolerance=1e-5) + assert_near_equal(prob["T_cond"], np.ones(nn) * 29.9441105, tolerance=1e-5) - partials = prob.check_partials(method='cs',compact_print=True, step=1e-50) + partials = prob.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) - + def test_simple_vector(self): nn = 5 prob = Problem() - pipe = prob.model.add_subsystem('test', HeatPipe(num_nodes=nn), promotes=['*']) - pipe.set_input_defaults('T_evap', units='degC', val=np.linspace(30, 60, nn)) - pipe.set_input_defaults('q', units='W', val=np.linspace(400, 1000, nn)) - pipe.set_input_defaults('length', units='m', val=10.22) - pipe.set_input_defaults('inner_diam', units='inch', val=.902) - pipe.set_input_defaults('n_pipes', val=1.) - pipe.set_input_defaults('T_design', units='degC', val=40) + pipe = prob.model.add_subsystem("test", HeatPipe(num_nodes=nn), promotes=["*"]) + pipe.set_input_defaults("T_evap", units="degC", val=np.linspace(30, 60, nn)) + pipe.set_input_defaults("q", units="W", val=np.linspace(400, 1000, nn)) + pipe.set_input_defaults("length", units="m", val=10.22) + pipe.set_input_defaults("inner_diam", units="inch", val=0.902) + pipe.set_input_defaults("n_pipes", val=1.0) + pipe.set_input_defaults("T_design", units="degC", val=40) prob.setup(check=True, force_alloc_complex=True) prob.run_model() - assert_near_equal(prob['q_max'], np.array([4936.70020611, 5022.24211291, 5073.99208835, 5095.24271287, 5080.99742858]), tolerance=1e-5) - assert_near_equal(prob['weight'], 0.51463886, tolerance=1e-5) - assert_near_equal(prob['T_cond'], np.array([29.9441105, 37.4231564, 44.90220466, 52.38125436, 59.86030483]), tolerance=1e-5) - - partials = prob.check_partials(method='cs',compact_print=True, step=1e-50) + assert_near_equal( + prob["q_max"], + np.array([4936.70020611, 5022.24211291, 5073.99208835, 5095.24271287, 5080.99742858]), + tolerance=1e-5, + ) + assert_near_equal(prob["weight"], 0.51463886, tolerance=1e-5) + assert_near_equal( + prob["T_cond"], np.array([29.9441105, 37.4231564, 44.90220466, 52.38125436, 59.86030483]), tolerance=1e-5 + ) + + partials = prob.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) - + def test_two_pipes(self): nn = 3 prob = Problem() # Run one and two pipes to compare results one = Problem() - pipe = one.model.add_subsystem('test', HeatPipe(num_nodes=nn), promotes=['*']) - pipe.set_input_defaults('T_evap', units='degC', val=np.linspace(30, 30, nn)) - pipe.set_input_defaults('q', units='W', val=np.linspace(200, 200, nn)) - pipe.set_input_defaults('length', units='m', val=10.22) - pipe.set_input_defaults('inner_diam', units='inch', val=.702) - pipe.set_input_defaults('n_pipes', val=1.) - pipe.set_input_defaults('T_design', units='degC', val=70) + pipe = one.model.add_subsystem("test", HeatPipe(num_nodes=nn), promotes=["*"]) + pipe.set_input_defaults("T_evap", units="degC", val=np.linspace(30, 30, nn)) + pipe.set_input_defaults("q", units="W", val=np.linspace(200, 200, nn)) + pipe.set_input_defaults("length", units="m", val=10.22) + pipe.set_input_defaults("inner_diam", units="inch", val=0.702) + pipe.set_input_defaults("n_pipes", val=1.0) + pipe.set_input_defaults("T_design", units="degC", val=70) one.setup(check=True, force_alloc_complex=True) one.run_model() - partials = one.check_partials(method='cs',compact_print=True, step=1e-50) + partials = one.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) # Twice as many pipes with twice as much heat two = Problem() - pipe = two.model.add_subsystem('test', HeatPipe(num_nodes=nn), promotes=['*']) - pipe.set_input_defaults('T_evap', units='degC', val=np.linspace(30, 30, nn)) - pipe.set_input_defaults('q', units='W', val=np.linspace(400, 400, nn)) - pipe.set_input_defaults('length', units='m', val=10.22) - pipe.set_input_defaults('inner_diam', units='inch', val=.702) - pipe.set_input_defaults('n_pipes', val=2.) - pipe.set_input_defaults('T_design', units='degC', val=70) + pipe = two.model.add_subsystem("test", HeatPipe(num_nodes=nn), promotes=["*"]) + pipe.set_input_defaults("T_evap", units="degC", val=np.linspace(30, 30, nn)) + pipe.set_input_defaults("q", units="W", val=np.linspace(400, 400, nn)) + pipe.set_input_defaults("length", units="m", val=10.22) + pipe.set_input_defaults("inner_diam", units="inch", val=0.702) + pipe.set_input_defaults("n_pipes", val=2.0) + pipe.set_input_defaults("T_design", units="degC", val=70) two.setup(check=True, force_alloc_complex=True) two.run_model() - partials = two.check_partials(method='cs',compact_print=True, step=1e-50) + partials = two.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) - assert_near_equal(one['q_max'], two['q_max']/2) - assert_near_equal(one['weight'], two['weight']/2) - assert_near_equal(one['T_cond'], two['T_cond']) + assert_near_equal(one["q_max"], two["q_max"] / 2) + assert_near_equal(one["weight"], two["weight"] / 2) + assert_near_equal(one["T_cond"], two["T_cond"]) + class HeatPipeThermalResistanceTestCase(unittest.TestCase): """ Basic test for HeatPipeThermalResistance component to ensure no drastic changes in outputs """ + def test_default_settings(self): nn = 3 p = Problem() - p.model.add_subsystem('test', HeatPipeThermalResistance(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", HeatPipeThermalResistance(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) p.run_model() - assert_near_equal(p['thermal_resistance'], np.ones(nn)*0.00076513, tolerance=1e-5) + assert_near_equal(p["thermal_resistance"], np.ones(nn) * 0.00076513, tolerance=1e-5) - partials = p.check_partials(method='cs',compact_print=True, step=1e-50) + partials = p.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) + class HeatPipeVaporTempDropTestCase(unittest.TestCase): """ Basic test for HeatPipeVaporTempDrop component to ensure no drastic changes in outputs """ + def test_default_settings(self): nn = 3 p = Problem() - p.model.add_subsystem('test', HeatPipeVaporTempDrop(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", HeatPipeVaporTempDrop(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) p.run_model() - - assert_near_equal(p['delta_T'], np.ones(nn)*2.37127, tolerance=1e-5) - partials = p.check_partials(method='cs',compact_print=True, step=1e-50) + assert_near_equal(p["delta_T"], np.ones(nn) * 2.37127, tolerance=1e-5) + + partials = p.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) + class HeatPipeWeightTestCase(unittest.TestCase): """ Basic test for HeatPipeWeight component to ensure no drastic changes in outputs """ + def test_default_settings(self): p = Problem() - p.model.add_subsystem('test', HeatPipeWeight(), promotes=['*']) + p.model.add_subsystem("test", HeatPipeWeight(), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) p.run_model() - - assert_near_equal(p['heat_pipe_weight'], 0.04074404, tolerance=1e-5) - assert_near_equal(p['wall_thickness'], 6.99300699e-05, tolerance=1e-5) - partials = p.check_partials(method='cs',compact_print=True) + + assert_near_equal(p["heat_pipe_weight"], 0.04074404, tolerance=1e-5) + assert_near_equal(p["wall_thickness"], 6.99300699e-05, tolerance=1e-5) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class AmmoniaPropertiesTestCase(unittest.TestCase): """ Basic test for AmmoniaProperties component to ensure no drastic changes in outputs """ + def test_on_data(self): nn = 3 p = Problem() - comp = p.model.add_subsystem('test', AmmoniaProperties(num_nodes=nn), promotes=['*']) - comp.set_input_defaults('temp', units='degC', val=np.ones(nn)*90.) + comp = p.model.add_subsystem("test", AmmoniaProperties(num_nodes=nn), promotes=["*"]) + comp.set_input_defaults("temp", units="degC", val=np.ones(nn) * 90.0) p.setup(check=True, force_alloc_complex=True) p.run_model() - - assert_near_equal(p['rho_liquid'], np.ones(nn)*482.9) - assert_near_equal(p['rho_vapor'], np.ones(nn)*43.9) - assert_near_equal(p['vapor_pressure'], np.ones(nn)*5123.) - partials = p.check_partials(method='cs',compact_print=True) + + assert_near_equal(p["rho_liquid"], np.ones(nn) * 482.9) + assert_near_equal(p["rho_vapor"], np.ones(nn) * 43.9) + assert_near_equal(p["vapor_pressure"], np.ones(nn) * 5123.0) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_interpolated(self): nn = 6 p = Problem() - comp = p.model.add_subsystem('test', AmmoniaProperties(num_nodes=nn), promotes=['*']) - comp.set_input_defaults('temp', units='degC', val=np.linspace(-7., 78., nn)) + comp = p.model.add_subsystem("test", AmmoniaProperties(num_nodes=nn), promotes=["*"]) + comp.set_input_defaults("temp", units="degC", val=np.linspace(-7.0, 78.0, nn)) p.setup(check=True, force_alloc_complex=True) p.run_model() - - assert_near_equal(p['rho_liquid'], np.array([648.00187402, 624.69, 599.75101299, 572.91683838, 543.40564864, 509.97440705]), tolerance=1e-5) - assert_near_equal(p['rho_vapor'], np.array([2.6756274, 4.8593, 8.26745558, 13.40464169, 21.03778235, 32.43929276]), tolerance=1e-5) - assert_near_equal(p['vapor_pressure'], np.array([327.98889865, 614.9, 1065.92300458, 1733.70063068, 2677.30942723, 3966.79472967]), tolerance=1e-5) - partials = p.check_partials(method='cs',compact_print=True) + + assert_near_equal( + p["rho_liquid"], + np.array([648.00187402, 624.69, 599.75101299, 572.91683838, 543.40564864, 509.97440705]), + tolerance=1e-5, + ) + assert_near_equal( + p["rho_vapor"], + np.array([2.6756274, 4.8593, 8.26745558, 13.40464169, 21.03778235, 32.43929276]), + tolerance=1e-5, + ) + assert_near_equal( + p["vapor_pressure"], + np.array([327.98889865, 614.9, 1065.92300458, 1733.70063068, 2677.30942723, 3966.79472967]), + tolerance=1e-5, + ) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class QMaxHeatPipeTestCase(unittest.TestCase): """ Basic test for QMaxHeatPipe component to ensure no drastic changes in outputs """ + def test_default_settings(self): nn = 3 p = Problem() - comp = p.model.add_subsystem('test', QMaxHeatPipe(num_nodes=nn), promotes=['*']) - comp.set_input_defaults('temp', units='degC', val=np.linspace(30, 60, nn)) - comp.set_input_defaults('length', units='m', val=10.22) - comp.set_input_defaults('inner_diam', units='inch', val=.902) - comp.set_input_defaults('design_temp', units='degC', val=40) + comp = p.model.add_subsystem("test", QMaxHeatPipe(num_nodes=nn), promotes=["*"]) + comp.set_input_defaults("temp", units="degC", val=np.linspace(30, 60, nn)) + comp.set_input_defaults("length", units="m", val=10.22) + comp.set_input_defaults("inner_diam", units="inch", val=0.902) + comp.set_input_defaults("design_temp", units="degC", val=40) p.setup(check=True, force_alloc_complex=True) p.run_model() - - assert_near_equal(p['q_max'], np.array([4936.70020611, 5073.99208835, 5080.99742858]), tolerance=1e-5) - assert_near_equal(p['heat_pipe_weight'], 0.51463886, tolerance=1e-5) - partials = p.check_partials(method='cs',compact_print=True, step=1e-50) + assert_near_equal(p["q_max"], np.array([4936.70020611, 5073.99208835, 5080.99742858]), tolerance=1e-5) + assert_near_equal(p["heat_pipe_weight"], 0.51463886, tolerance=1e-5) + + partials = p.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) + class QMaxAnalyticalPartTestCase(unittest.TestCase): """ Basic test for QMaxAnalyticalPart component to ensure no drastic changes in outputs """ + def test_default_settings(self): nn = 3 p = Problem() - p.model.add_subsystem('test', QMaxAnalyticalPart(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", QMaxAnalyticalPart(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) p.run_model() - - assert_near_equal(p['q_max'], np.ones(nn)*875.85211163, tolerance=1e-5) - partials = p.check_partials(method='cs',compact_print=True, step=1e-50) + assert_near_equal(p["q_max"], np.ones(nn) * 875.85211163, tolerance=1e-5) + + partials = p.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) -if __name__=="__main__": + +if __name__ == "__main__": unittest.main() diff --git a/openconcept/thermal/tests/test_hose.py b/openconcept/thermal/tests/test_hose.py index 34db35dc..3b6058d8 100644 --- a/openconcept/thermal/tests/test_hose.py +++ b/openconcept/thermal/tests/test_hose.py @@ -14,29 +14,29 @@ def generate_model(self, nn): prob = om.Problem() hose_diam = 0.02 - hose_length = 16. + hose_length = 16.0 hose_design_pressure = 1e6 mdot_coolant = np.linspace(0.6, 1.2, nn) - rho_coolant = 1020*np.ones((nn,)) + rho_coolant = 1020 * np.ones((nn,)) mu_coolant = 1.68e-3 sigma = 2.07e6 rho_hose = 1356.3 - ivc = prob.model.add_subsystem('ivc', om.IndepVarComp(), promotes_outputs=['*']) - ivc.add_output('hose_diameter', val=hose_diam, units='m') - ivc.add_output('hose_length', val=hose_length, units='m') - ivc.add_output('hose_design_pressure', val=hose_design_pressure, units='Pa') - ivc.add_output('mdot_coolant', val=mdot_coolant, units='kg/s') - ivc.add_output('rho_coolant', val=rho_coolant, units='kg/m**3') - ivc.add_output('mu_coolant', val=mu_coolant, units='kg/m/s') - prob.model.add_subsystem('hose', SimpleHose(num_nodes=nn), promotes_inputs=['*']) + ivc = prob.model.add_subsystem("ivc", om.IndepVarComp(), promotes_outputs=["*"]) + ivc.add_output("hose_diameter", val=hose_diam, units="m") + ivc.add_output("hose_length", val=hose_length, units="m") + ivc.add_output("hose_design_pressure", val=hose_design_pressure, units="Pa") + ivc.add_output("mdot_coolant", val=mdot_coolant, units="kg/s") + ivc.add_output("rho_coolant", val=rho_coolant, units="kg/m**3") + ivc.add_output("mu_coolant", val=mu_coolant, units="kg/m/s") + prob.model.add_subsystem("hose", SimpleHose(num_nodes=nn), promotes_inputs=["*"]) prob.setup(check=True, force_alloc_complex=True) - - xs_area = np.pi * (hose_diam / 2 )**2 + + xs_area = np.pi * (hose_diam / 2) ** 2 U = mdot_coolant / rho_coolant / xs_area Redh = rho_coolant * U * hose_diam / mu_coolant - f = 0.3164 * Redh ** (-1/4) - dp = f * rho_coolant / 2 * hose_length * U ** 2 / hose_diam + f = 0.3164 * Redh ** (-1 / 4) + dp = f * rho_coolant / 2 * hose_length * U**2 / hose_diam wall_thickness = hose_design_pressure * (hose_diam / 2) / sigma hose_weight = wall_thickness * np.pi * (hose_diam + wall_thickness) * rho_hose * hose_length @@ -46,22 +46,19 @@ def generate_model(self, nn): def test_scalar(self): prob, dp, weight = self.generate_model(nn=1) prob.run_model() - assert_near_equal(prob.get_val('hose.delta_p', units='Pa'), - dp, tolerance=1e-10) - assert_near_equal(prob.get_val('hose.component_weight', units='kg'), - weight, tolerance=1e-10) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("hose.delta_p", units="Pa"), dp, tolerance=1e-10) + assert_near_equal(prob.get_val("hose.component_weight", units="kg"), weight, tolerance=1e-10) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_vector(self): prob, dp, weight = self.generate_model(nn=11) prob.run_model() - assert_near_equal(prob.get_val('hose.delta_p', units='Pa'), - dp, tolerance=1e-10) - assert_near_equal(prob.get_val('hose.component_weight', units='kg'), - weight, tolerance=1e-10) - partials = prob.check_partials(method='cs',compact_print=True) - assert_check_partials(partials) + assert_near_equal(prob.get_val("hose.delta_p", units="Pa"), dp, tolerance=1e-10) + assert_near_equal(prob.get_val("hose.component_weight", units="kg"), weight, tolerance=1e-10) + partials = prob.check_partials(method="cs", compact_print=True) + assert_check_partials(partials) + if __name__ == "__main__": unittest.main() diff --git a/openconcept/thermal/tests/test_manifold.py b/openconcept/thermal/tests/test_manifold.py index 7ed11df7..a3d5cd10 100644 --- a/openconcept/thermal/tests/test_manifold.py +++ b/openconcept/thermal/tests/test_manifold.py @@ -9,74 +9,76 @@ class FlowSplitTestCase(unittest.TestCase): """ Test the FlowSplit component """ + def test_default_settings(self): p = Problem() - p.model.add_subsystem('test', FlowSplit(), promotes=['*']) + p.model.add_subsystem("test", FlowSplit(), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) p.run_model() - assert_near_equal(p['mdot_out_A'], np.array([0.5])) - assert_near_equal(p['mdot_out_B'], np.array([0.5])) + assert_near_equal(p["mdot_out_A"], np.array([0.5])) + assert_near_equal(p["mdot_out_B"], np.array([0.5])) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_nondefault_settings(self): nn = 4 p = Problem() - p.model.add_subsystem('test', FlowSplit(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", FlowSplit(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - - p['mdot_in'] = np.array([-10., 0., 10., 10.]) - p['mdot_split_fraction'] = np.array([0., 0.4, 0.4, 1.]) + + p["mdot_in"] = np.array([-10.0, 0.0, 10.0, 10.0]) + p["mdot_split_fraction"] = np.array([0.0, 0.4, 0.4, 1.0]) p.run_model() - assert_near_equal(p['mdot_out_A'], np.array([0., 0., 4., 10.])) - assert_near_equal(p['mdot_out_B'], np.array([-10., 0., 6., 0.])) + assert_near_equal(p["mdot_out_A"], np.array([0.0, 0.0, 4.0, 10.0])) + assert_near_equal(p["mdot_out_B"], np.array([-10.0, 0.0, 6.0, 0.0])) def test_warnings(self): nn = 4 p = Problem() - p.model.add_subsystem('test', FlowSplit(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", FlowSplit(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - - p['mdot_in'] = np.array([-10., 0., 10., 10.]) - p['mdot_split_fraction'] = np.array([-0.0001, 0.4, 0.4, 1.]) + + p["mdot_in"] = np.array([-10.0, 0.0, 10.0, 10.0]) + p["mdot_split_fraction"] = np.array([-0.0001, 0.4, 0.4, 1.0]) with self.assertRaises(RuntimeWarning): p.run_model() - - p['mdot_split_fraction'] = np.array([1.0001, 0.4, 0.4, 1.]) + p["mdot_split_fraction"] = np.array([1.0001, 0.4, 0.4, 1.0]) with self.assertRaises(RuntimeWarning): p.run_model() + class FlowCombineTestCase(unittest.TestCase): """ Test the FlowCombine component """ + def test_default_settings(self): p = Problem() - p.model.add_subsystem('test', FlowCombine(), promotes=['*']) + p.model.add_subsystem("test", FlowCombine(), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) p.run_model() - assert_near_equal(p['mdot_out'], np.array([2.])) - assert_near_equal(p['T_out'], np.array([1.])) + assert_near_equal(p["mdot_out"], np.array([2.0])) + assert_near_equal(p["T_out"], np.array([1.0])) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_nondefault_settings(self): nn = 4 p = Problem() - p.model.add_subsystem('test', FlowCombine(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", FlowCombine(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - - p['mdot_in_A'] = np.array([0., 5., 10., 10.]) - p['mdot_in_B'] = np.array([1., 0., 5., 10.]) - p['T_in_A'] = np.array([1., 10., 30., 500.]) - p['T_in_B'] = np.array([1., 150., 60., 100.]) + + p["mdot_in_A"] = np.array([0.0, 5.0, 10.0, 10.0]) + p["mdot_in_B"] = np.array([1.0, 0.0, 5.0, 10.0]) + p["T_in_A"] = np.array([1.0, 10.0, 30.0, 500.0]) + p["T_in_B"] = np.array([1.0, 150.0, 60.0, 100.0]) p.run_model() - assert_near_equal(p['mdot_out'], np.array([1., 5., 15., 20.])) - assert_near_equal(p['T_out'], np.array([1., 10., 40., 300.])) + assert_near_equal(p["mdot_out"], np.array([1.0, 5.0, 15.0, 20.0])) + assert_near_equal(p["T_out"], np.array([1.0, 10.0, 40.0, 300.0])) diff --git a/openconcept/thermal/tests/test_motor_cooling.py b/openconcept/thermal/tests/test_motor_cooling.py index 7dce19ed..ee80284c 100644 --- a/openconcept/thermal/tests/test_motor_cooling.py +++ b/openconcept/thermal/tests/test_motor_cooling.py @@ -4,19 +4,21 @@ import numpy as np from openconcept.thermal import LiquidCooledMotor + class QuasiSteadyMotorCoolingTestCase(unittest.TestCase): """ Test the liquid cooled motor in quasi-steady (massless) mode """ + def generate_model(self, nn): prob = om.Problem() - ivc = prob.model.add_subsystem('ivc', om.IndepVarComp(), promotes_outputs=['*']) - ivc.add_output('q_in', val=np.ones((nn,))*10000, units='W') - ivc.add_output('T_in', 25.*np.ones((nn,)), units='degC') - ivc.add_output('mdot_coolant', 3.0*np.ones((nn,)), units='kg/s') - ivc.add_output('motor_weight', 40, units='kg') - ivc.add_output('power_rating', 200, units='kW') - prob.model.add_subsystem('lcm', LiquidCooledMotor(num_nodes=nn, quasi_steady=True), promotes_inputs=['*']) + ivc = prob.model.add_subsystem("ivc", om.IndepVarComp(), promotes_outputs=["*"]) + ivc.add_output("q_in", val=np.ones((nn,)) * 10000, units="W") + ivc.add_output("T_in", 25.0 * np.ones((nn,)), units="degC") + ivc.add_output("mdot_coolant", 3.0 * np.ones((nn,)), units="kg/s") + ivc.add_output("motor_weight", 40, units="kg") + ivc.add_output("power_rating", 200, units="kW") + prob.model.add_subsystem("lcm", LiquidCooledMotor(num_nodes=nn, quasi_steady=True), promotes_inputs=["*"]) prob.model.nonlinear_solver = om.NewtonSolver(solve_subsystems=True) prob.model.linear_solver = om.DirectSolver() prob.setup(check=True, force_alloc_complex=True) @@ -29,16 +31,16 @@ def test_scalar(self): mdot_coolant = 3.0 q_generated = power_rating * 0.05 cp_coolant = 3801 - UA = 1100/650000*power_rating - Cmin = cp_coolant * mdot_coolant # cp * mass flow rate - NTU = UA/Cmin + UA = 1100 / 650000 * power_rating + Cmin = cp_coolant * mdot_coolant # cp * mass flow rate + NTU = UA / Cmin T_in = 298.15 effectiveness = 1 - np.exp(-NTU) delta_T = q_generated / effectiveness / Cmin - assert_near_equal(prob.get_val('lcm.dTdt'), 0.0, tolerance=1e-14) - assert_near_equal(prob.get_val('lcm.T', units='K'), T_in + delta_T, tolerance=1e-10) - assert_near_equal(prob.get_val('lcm.T_out', units='K'), T_in + q_generated / Cmin, tolerance=1e-10) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("lcm.dTdt"), 0.0, tolerance=1e-14) + assert_near_equal(prob.get_val("lcm.T", units="K"), T_in + delta_T, tolerance=1e-10) + assert_near_equal(prob.get_val("lcm.T_out", units="K"), T_in + q_generated / Cmin, tolerance=1e-10) + partials = prob.check_partials(method="cs", compact_print=True) # prob.model.list_outputs(print_arrays=True, units=True) assert_check_partials(partials) @@ -49,23 +51,27 @@ def test_vector(self): mdot_coolant = 3.0 q_generated = power_rating * 0.05 cp_coolant = 3801 - UA = 1100/650000*power_rating - Cmin = cp_coolant * mdot_coolant # cp * mass flow rate - NTU = UA/Cmin + UA = 1100 / 650000 * power_rating + Cmin = cp_coolant * mdot_coolant # cp * mass flow rate + NTU = UA / Cmin T_in = 298.15 effectiveness = 1 - np.exp(-NTU) delta_T = q_generated / effectiveness / Cmin - assert_near_equal(prob.get_val('lcm.dTdt'), np.zeros((11,)), tolerance=1e-14) - assert_near_equal(prob.get_val('lcm.T', units='K'), np.ones((11,))*(T_in + delta_T), tolerance=1e-10) - assert_near_equal(prob.get_val('lcm.T_out', units='K'), np.ones((11,))*(T_in + q_generated / Cmin), tolerance=1e-10) + assert_near_equal(prob.get_val("lcm.dTdt"), np.zeros((11,)), tolerance=1e-14) + assert_near_equal(prob.get_val("lcm.T", units="K"), np.ones((11,)) * (T_in + delta_T), tolerance=1e-10) + assert_near_equal( + prob.get_val("lcm.T_out", units="K"), np.ones((11,)) * (T_in + q_generated / Cmin), tolerance=1e-10 + ) # prob.model.list_outputs(print_arrays=True, units='True') - partials = prob.check_partials(method='cs',compact_print=True) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class UnsteadyMotorCoolingTestCase(unittest.TestCase): """ Test the liquid cooled motor in unsteady mode """ + def generate_model(self, nn): """ An example demonstrating unsteady motor cooling @@ -76,28 +82,34 @@ def generate_model(self, nn): class VehicleModel(om.Group): def initialize(self): - self.options.declare('num_nodes', default=11) - + self.options.declare("num_nodes", default=11) + def setup(self): - num_nodes = self.options['num_nodes'] - ivc = self.add_subsystem('ivc', om.IndepVarComp(), promotes_outputs=['*']) - ivc.add_output('q_in', val=np.ones((num_nodes,))*10000, units='W') - ivc.add_output('T_in', 25.*np.ones((num_nodes,)), units='degC') - ivc.add_output('mdot_coolant', 3.0*np.ones((num_nodes,)), units='kg/s') - ivc.add_output('motor_weight', 40, units='kg') - ivc.add_output('power_rating', 200, units='kW') - self.add_subsystem('lcm', LiquidCooledMotor(num_nodes=num_nodes, quasi_steady=False), promotes_inputs=['*']) + num_nodes = self.options["num_nodes"] + ivc = self.add_subsystem("ivc", om.IndepVarComp(), promotes_outputs=["*"]) + ivc.add_output("q_in", val=np.ones((num_nodes,)) * 10000, units="W") + ivc.add_output("T_in", 25.0 * np.ones((num_nodes,)), units="degC") + ivc.add_output("mdot_coolant", 3.0 * np.ones((num_nodes,)), units="kg/s") + ivc.add_output("motor_weight", 40, units="kg") + ivc.add_output("power_rating", 200, units="kW") + self.add_subsystem( + "lcm", LiquidCooledMotor(num_nodes=num_nodes, quasi_steady=False), promotes_inputs=["*"] + ) class TrajectoryPhase(PhaseGroup): "An OpenConcept Phase comprises part of a time-based TrajectoryGroup and always needs to have a 'duration' defined" + def setup(self): - self.add_subsystem('ivc', om.IndepVarComp('duration', val=20, units='min'), promotes_outputs=['duration']) - self.add_subsystem('vm', VehicleModel(num_nodes=self.options['num_nodes'])) + self.add_subsystem( + "ivc", om.IndepVarComp("duration", val=20, units="min"), promotes_outputs=["duration"] + ) + self.add_subsystem("vm", VehicleModel(num_nodes=self.options["num_nodes"])) class Trajectory(TrajectoryGroup): "An OpenConcept TrajectoryGroup consists of one or more phases that may be linked together. This will often be a top-level model" + def setup(self): - self.add_subsystem('phase1', TrajectoryPhase(num_nodes=nn)) + self.add_subsystem("phase1", TrajectoryPhase(num_nodes=nn)) # self.add_subsystem('phase2', TrajectoryPhase(num_nodes=nn)) # the link_phases directive ensures continuity of state variables across phase boundaries # self.link_phases(self.phase1, self.phase2) @@ -105,17 +117,17 @@ def setup(self): prob = om.Problem(Trajectory()) prob.model.nonlinear_solver = om.NewtonSolver(iprint=2) prob.model.linear_solver = om.DirectSolver() - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.nonlinear_solver.options['maxiter'] = 20 - prob.model.nonlinear_solver.options['atol'] = 1e-6 - prob.model.nonlinear_solver.options['rtol'] = 1e-6 + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.nonlinear_solver.options["maxiter"] = 20 + prob.model.nonlinear_solver.options["atol"] = 1e-6 + prob.model.nonlinear_solver.options["rtol"] = 1e-6 prob.setup(force_alloc_complex=True) # set the initial value of the state at the beginning of the TrajectoryGroup - prob['phase1.vm.T_initial'] = 300. + prob["phase1.vm.T_initial"] = 300.0 prob.run_model() # prob.model.list_outputs(print_arrays=True, units=True) # prob.model.list_inputs(print_arrays=True, units=True) - + return prob def test_vector(self): @@ -125,27 +137,56 @@ def test_vector(self): mdot_coolant = 3.0 q_generated = power_rating * 0.05 cp_coolant = 3801 - UA = 1100/650000*power_rating - Cmin = cp_coolant * mdot_coolant # cp * mass flow rate - NTU = UA/Cmin + UA = 1100 / 650000 * power_rating + Cmin = cp_coolant * mdot_coolant # cp * mass flow rate + NTU = UA / Cmin T_in = 298.15 effectiveness = 1 - np.exp(-NTU) delta_T = q_generated / effectiveness / Cmin - assert_near_equal(prob.get_val('phase1.vm.lcm.T', units='K'), - np.array([300. , 319.02071102, 324.65196197, 327.0073297 , - 327.7046573 , 327.99632659, 328.08267788, 328.11879579, - 328.12948882, 328.13396137, 328.1352855]), tolerance=1e-10) - assert_near_equal(prob.get_val('phase1.vm.lcm.T_out', units='K'), - np.array([298.2041044 , 298.76037687, 298.92506629, 298.99395048, - 299.01434425, 299.0228743 , 299.0253997 , 299.02645599, - 299.02676872, 299.02689952, 299.02693824]), tolerance=1e-10) - assert_near_equal(prob.get_val('phase1.vm.lcm.T', units='K')[0], - np.array([300.]), tolerance=1e-10) + assert_near_equal( + prob.get_val("phase1.vm.lcm.T", units="K"), + np.array( + [ + 300.0, + 319.02071102, + 324.65196197, + 327.0073297, + 327.7046573, + 327.99632659, + 328.08267788, + 328.11879579, + 328.12948882, + 328.13396137, + 328.1352855, + ] + ), + tolerance=1e-10, + ) + assert_near_equal( + prob.get_val("phase1.vm.lcm.T_out", units="K"), + np.array( + [ + 298.2041044, + 298.76037687, + 298.92506629, + 298.99395048, + 299.01434425, + 299.0228743, + 299.0253997, + 299.02645599, + 299.02676872, + 299.02689952, + 299.02693824, + ] + ), + tolerance=1e-10, + ) + assert_near_equal(prob.get_val("phase1.vm.lcm.T", units="K")[0], np.array([300.0]), tolerance=1e-10) # at the end of the period the unsteady value should be approx the quasi-steady value - assert_near_equal(prob.get_val('phase1.vm.lcm.T', units='K')[-1], - np.array([T_in + delta_T]), tolerance=1e-5) - assert_near_equal(prob.get_val('phase1.vm.lcm.T_out', units='K')[-1], - np.array([T_in + q_generated / Cmin]), tolerance=1e-5) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("phase1.vm.lcm.T", units="K")[-1], np.array([T_in + delta_T]), tolerance=1e-5) + assert_near_equal( + prob.get_val("phase1.vm.lcm.T_out", units="K")[-1], np.array([T_in + q_generated / Cmin]), tolerance=1e-5 + ) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) diff --git a/openconcept/thermal/tests/test_pump.py b/openconcept/thermal/tests/test_pump.py index 859bc7ec..3cedca70 100644 --- a/openconcept/thermal/tests/test_pump.py +++ b/openconcept/thermal/tests/test_pump.py @@ -15,18 +15,18 @@ def generate_model(self, nn): efficiency = 0.35 spec_power = 1 / 450 - rho_coolant = 1020*np.ones(nn) + rho_coolant = 1020 * np.ones(nn) mdot_coolant = np.linspace(0.6, 1.2, nn) delta_p = np.linspace(2e4, 4e4, nn) power_rating = 1000 - ivc = prob.model.add_subsystem('ivc', om.IndepVarComp(), promotes_outputs=['*']) - ivc.add_output('power_rating', val=power_rating, units='W') - ivc.add_output('delta_p', val=delta_p, units='Pa') - ivc.add_output('mdot_coolant', val=mdot_coolant, units='kg/s') - ivc.add_output('rho_coolant', val=rho_coolant, units='kg/m**3') - prob.model.add_subsystem('pump', SimplePump(num_nodes=nn), promotes_inputs=['*']) + ivc = prob.model.add_subsystem("ivc", om.IndepVarComp(), promotes_outputs=["*"]) + ivc.add_output("power_rating", val=power_rating, units="W") + ivc.add_output("delta_p", val=delta_p, units="Pa") + ivc.add_output("mdot_coolant", val=mdot_coolant, units="kg/s") + ivc.add_output("rho_coolant", val=rho_coolant, units="kg/m**3") + prob.model.add_subsystem("pump", SimplePump(num_nodes=nn), promotes_inputs=["*"]) prob.setup(check=True, force_alloc_complex=True) - + fluid_power = (mdot_coolant / rho_coolant) * delta_p weight = power_rating * spec_power elec_load = fluid_power / efficiency @@ -37,28 +37,21 @@ def generate_model(self, nn): def test_scalar(self): prob, elec_load, weight, margin = self.generate_model(nn=1) prob.run_model() - assert_near_equal(prob.get_val('pump.elec_load', units='W'), - elec_load, tolerance=1e-10) - assert_near_equal(prob.get_val('pump.component_weight', units='kg'), - weight, tolerance=1e-10) - assert_near_equal(prob.get_val('pump.component_sizing_margin', units=None), - margin, tolerance=1e-10) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("pump.elec_load", units="W"), elec_load, tolerance=1e-10) + assert_near_equal(prob.get_val("pump.component_weight", units="kg"), weight, tolerance=1e-10) + assert_near_equal(prob.get_val("pump.component_sizing_margin", units=None), margin, tolerance=1e-10) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) def test_scalar(self): prob, elec_load, weight, margin = self.generate_model(nn=11) prob.run_model() - assert_near_equal(prob.get_val('pump.elec_load', units='W'), - elec_load, tolerance=1e-10) - assert_near_equal(prob.get_val('pump.component_weight', units='kg'), - weight, tolerance=1e-10) - assert_near_equal(prob.get_val('pump.component_sizing_margin', units=None), - margin, tolerance=1e-10) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("pump.elec_load", units="W"), elec_load, tolerance=1e-10) + assert_near_equal(prob.get_val("pump.component_weight", units="kg"), weight, tolerance=1e-10) + assert_near_equal(prob.get_val("pump.component_sizing_margin", units=None), margin, tolerance=1e-10) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + if __name__ == "__main__": unittest.main() - - diff --git a/openconcept/thermal/tests/test_thermal_comps.py b/openconcept/thermal/tests/test_thermal_comps.py index 74995135..e6bfd537 100644 --- a/openconcept/thermal/tests/test_thermal_comps.py +++ b/openconcept/thermal/tests/test_thermal_comps.py @@ -11,159 +11,184 @@ CoolantReservoir, ) + class PerfectHeatTransferCompTestCase(unittest.TestCase): """ Test the PerfectHeatTransferComp component """ + def test_comp(self): num_nodes = 3 prob = Problem() - prob.model.add_subsystem('test', PerfectHeatTransferComp(num_nodes=num_nodes), promotes=['*']) + prob.model.add_subsystem("test", PerfectHeatTransferComp(num_nodes=num_nodes), promotes=["*"]) prob.setup(check=True, force_alloc_complex=True) # Set the values - prob['T_in'] = np.array([300., 350., 400.]) - prob['q'] = np.array([10000., 0., -10000.]) - prob['mdot_coolant'] = np.array([1., 1., 1.]) + prob["T_in"] = np.array([300.0, 350.0, 400.0]) + prob["q"] = np.array([10000.0, 0.0, -10000.0]) + prob["mdot_coolant"] = np.array([1.0, 1.0, 1.0]) prob.run_model() - dT_coolant = 10000./3801. + dT_coolant = 10000.0 / 3801.0 - assert_near_equal(prob['T_out'], np.array([300 + dT_coolant, 350., 400 - dT_coolant])) - assert_near_equal(prob['T_average'], np.array([300 + dT_coolant/2, 350., 400 - dT_coolant/2])) + assert_near_equal(prob["T_out"], np.array([300 + dT_coolant, 350.0, 400 - dT_coolant])) + assert_near_equal(prob["T_average"], np.array([300 + dT_coolant / 2, 350.0, 400 - dT_coolant / 2])) - partials = prob.check_partials(method='cs',compact_print=True) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class ThermalComponentWithMassTestCase(unittest.TestCase): """ Test the ThermalComponentWithMass component """ + def test_comp(self): num_nodes = 3 prob = Problem() - prob.model.add_subsystem('test', ThermalComponentWithMass(num_nodes=num_nodes), promotes=['*']) + prob.model.add_subsystem("test", ThermalComponentWithMass(num_nodes=num_nodes), promotes=["*"]) prob.setup(check=True, force_alloc_complex=True) # Set the values - prob.set_val('q_in', np.array([10., 14., 4.]), units='kW') - prob.set_val('q_out', np.array([9., 14., 12.]), units='kW') - prob.set_val('mass', 7., units='kg') + prob.set_val("q_in", np.array([10.0, 14.0, 4.0]), units="kW") + prob.set_val("q_out", np.array([9.0, 14.0, 12.0]), units="kW") + prob.set_val("mass", 7.0, units="kg") prob.run_model() - assert_near_equal(prob.get_val('dTdt', units='K/s'), np.array([.1551109043, 0., -1.2408872344]), tolerance=1e-9) + assert_near_equal( + prob.get_val("dTdt", units="K/s"), np.array([0.1551109043, 0.0, -1.2408872344]), tolerance=1e-9 + ) - partials = prob.check_partials(method='cs',compact_print=True) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class ColdPlateTestCase(unittest.TestCase): """ Test the ConstantSurfaceTemperatureColdPlate_NTU component """ + def test_comp(self): num_nodes = 3 prob = Problem() - prob.model.add_subsystem('test', ConstantSurfaceTemperatureColdPlate_NTU(num_nodes=num_nodes), promotes=['*']) + prob.model.add_subsystem("test", ConstantSurfaceTemperatureColdPlate_NTU(num_nodes=num_nodes), promotes=["*"]) prob.setup(check=True, force_alloc_complex=True) # Set the values - prob.set_val('T_in', np.array([300., 350., 290.]), units='K') - prob.set_val('T_surface', np.array([350., 500., 250.]), units='K') - prob.set_val('mdot_coolant', np.array([9., 14., 12.]), units='kg/s') - prob.set_val('channel_length', 7., units='mm') - prob.set_val('channel_width', 1., units='mm') - prob.set_val('channel_height', .5, units='mm') - prob.set_val('n_parallel', 5) + prob.set_val("T_in", np.array([300.0, 350.0, 290.0]), units="K") + prob.set_val("T_surface", np.array([350.0, 500.0, 250.0]), units="K") + prob.set_val("mdot_coolant", np.array([9.0, 14.0, 12.0]), units="kg/s") + prob.set_val("channel_length", 7.0, units="mm") + prob.set_val("channel_width", 1.0, units="mm") + prob.set_val("channel_height", 0.5, units="mm") + prob.set_val("n_parallel", 5) prob.run_model() - assert_near_equal(prob.get_val('q', units='W'), np.array([24.04771845, 72.14333648, -19.23820857]), tolerance=1e-9) - assert_near_equal(prob.get_val('T_out', units='K'), np.array([300.00070296, 350.00135572, 289.99957822]), tolerance=1e-9) + assert_near_equal( + prob.get_val("q", units="W"), np.array([24.04771845, 72.14333648, -19.23820857]), tolerance=1e-9 + ) + assert_near_equal( + prob.get_val("T_out", units="K"), np.array([300.00070296, 350.00135572, 289.99957822]), tolerance=1e-9 + ) + class LiquidCooledCompTestCase(unittest.TestCase): """ Test the LiquidCooledComp component """ + def test_comp(self): num_nodes = 3 prob = Problem() - prob.model.nonlinear_solver=NewtonSolver() + prob.model.nonlinear_solver = NewtonSolver() prob.model.linear_solver = DirectSolver() - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.add_subsystem('test', LiquidCooledComp(num_nodes=num_nodes), promotes=['*']) + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.add_subsystem("test", LiquidCooledComp(num_nodes=num_nodes), promotes=["*"]) prob.setup(check=True, force_alloc_complex=True) # Set the values - prob.set_val('q_in', np.array([1000., 1400., 4000.]), units='W') - prob.set_val('mdot_coolant', np.array([9., 14., 12.]), units='kg/s') - prob.set_val('T_in', np.array([300., 350., 290.]), units='K') - prob.set_val('mass', 10., units='kg') - prob.set_val('T_initial', 400., units='K') - prob.set_val('duration', 2., units='min') - prob.set_val('channel_length', 7., units='mm') - prob.set_val('channel_width', 1., units='mm') - prob.set_val('channel_height', .5, units='mm') - prob.set_val('n_parallel', 5) + prob.set_val("q_in", np.array([1000.0, 1400.0, 4000.0]), units="W") + prob.set_val("mdot_coolant", np.array([9.0, 14.0, 12.0]), units="kg/s") + prob.set_val("T_in", np.array([300.0, 350.0, 290.0]), units="K") + prob.set_val("mass", 10.0, units="kg") + prob.set_val("T_initial", 400.0, units="K") + prob.set_val("duration", 2.0, units="min") + prob.set_val("channel_length", 7.0, units="mm") + prob.set_val("channel_width", 1.0, units="mm") + prob.set_val("channel_height", 0.5, units="mm") + prob.set_val("n_parallel", 5) prob.run_model() - assert_near_equal(prob.get_val('T', units='K'), np.array([400., 406.40945984, 422.53992837]), tolerance=1e-9) - assert_near_equal(prob.get_val('T_out', units='K'), np.array([300.00140593, 350.00050984, 290.00139757]), tolerance=1e-9) + assert_near_equal(prob.get_val("T", units="K"), np.array([400.0, 406.40945984, 422.53992837]), tolerance=1e-9) + assert_near_equal( + prob.get_val("T_out", units="K"), np.array([300.00140593, 350.00050984, 290.00139757]), tolerance=1e-9 + ) - partials = prob.check_partials(method='cs',compact_print=True, step=1e-50) + partials = prob.check_partials(method="cs", compact_print=True, step=1e-50) assert_check_partials(partials) + class CoolantReservoirRateTestCase(unittest.TestCase): """ Test the CoolantReservoirRate component """ + def test_comp(self): num_nodes = 3 prob = Problem() - prob.model.add_subsystem('test', CoolantReservoirRate(num_nodes=num_nodes), promotes=['*']) + prob.model.add_subsystem("test", CoolantReservoirRate(num_nodes=num_nodes), promotes=["*"]) prob.setup(check=True, force_alloc_complex=True) # Set the values - prob.set_val('T_in', np.array([300., 350., 290.]), units='K') - prob.set_val('T_out', np.array([350., 340., 250.]), units='K') - prob.set_val('mdot_coolant', np.array([9., 14., 12.]), units='kg/s') - prob.set_val('mass', 7., units='kg') + prob.set_val("T_in", np.array([300.0, 350.0, 290.0]), units="K") + prob.set_val("T_out", np.array([350.0, 340.0, 250.0]), units="K") + prob.set_val("mdot_coolant", np.array([9.0, 14.0, 12.0]), units="kg/s") + prob.set_val("mass", 7.0, units="kg") prob.run_model() - assert_near_equal(prob.get_val('dTdt', units='K/s'), np.array([-64.28571429, 20., 68.57142857]), tolerance=1e-9) + assert_near_equal( + prob.get_val("dTdt", units="K/s"), np.array([-64.28571429, 20.0, 68.57142857]), tolerance=1e-9 + ) - partials = prob.check_partials(method='cs',compact_print=True) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class ReservoirTestCase(unittest.TestCase): """ Test the CoolantReservoir component """ + def test_comp(self): num_nodes = 3 prob = Problem() - prob.model.nonlinear_solver=NewtonSolver() + prob.model.nonlinear_solver = NewtonSolver() prob.model.linear_solver = DirectSolver() - prob.model.nonlinear_solver.options['solve_subsystems'] = True - prob.model.add_subsystem('test', CoolantReservoir(num_nodes=num_nodes), promotes=['*']) + prob.model.nonlinear_solver.options["solve_subsystems"] = True + prob.model.add_subsystem("test", CoolantReservoir(num_nodes=num_nodes), promotes=["*"]) prob.setup(check=True, force_alloc_complex=True) # Set the values - prob.set_val('mdot_coolant', np.array([9., 14., 12.]), units='kg/s') - prob.set_val('T_in', np.array([300., 350., 290.]), units='K') - prob.set_val('mass', 10., units='kg') - prob.set_val('T_initial', 400., units='K') - prob.set_val('duration', 20., units='min') + prob.set_val("mdot_coolant", np.array([9.0, 14.0, 12.0]), units="kg/s") + prob.set_val("T_in", np.array([300.0, 350.0, 290.0]), units="K") + prob.set_val("mass", 10.0, units="kg") + prob.set_val("T_initial", 400.0, units="K") + prob.set_val("duration", 20.0, units="min") prob.run_model() - assert_near_equal(prob.get_val('T_out', units='K'), np.array([400., 317.9653263, 364.64246726]), tolerance=1e-9) + assert_near_equal( + prob.get_val("T_out", units="K"), np.array([400.0, 317.9653263, 364.64246726]), tolerance=1e-9 + ) - partials = prob.check_partials(method='cs',compact_print=True) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) -if __name__=="__main__": + +if __name__ == "__main__": unittest.main() diff --git a/openconcept/thermal/thermal.py b/openconcept/thermal/thermal.py index 83e37e06..554accc6 100644 --- a/openconcept/thermal/thermal.py +++ b/openconcept/thermal/thermal.py @@ -2,6 +2,7 @@ import numpy as np from openconcept.utilities.math.integrals import Integrator + class PerfectHeatTransferComp(ExplicitComponent): """ Models heat transfer to coolant loop assuming zero thermal resistance. @@ -14,14 +15,14 @@ class PerfectHeatTransferComp(ExplicitComponent): Heat flow into fluid stream; positive is heat addition (vector, W) mdot_coolant : float Coolant mass flow (vector, kg/s) - + Outputs ------- T_out : float Outgoing coolant temperature (vector, K) T_average : float Average coolant temperature (vector K) - + Options ------- num_nodes : int @@ -29,34 +30,45 @@ class PerfectHeatTransferComp(ExplicitComponent): specific_heat : float Specific heat of the coolant (scalar, J/kg/K, default 3801 glycol/water) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - self.options.declare('specific_heat', default=3801., desc='Specific heat in J/kg/K') - + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + self.options.declare("specific_heat", default=3801.0, desc="Specific heat in J/kg/K") + def setup(self): - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] arange = np.arange(0, nn) - self.add_input('T_in', desc='Incoming coolant temp', units='K', shape=(nn,)) - self.add_input('q', desc='Heat INTO the fluid stream (positive is heat addition)', units='W', shape=(nn,)) - self.add_input('mdot_coolant', desc='Mass flow rate of coolant', units='kg/s', shape=(nn,)) - self.add_output('T_out', desc='Outgoing coolant temp', val=np.random.uniform(300, 330), lower=1e-10, units='K', shape=(nn,)) - self.add_output('T_average', desc='Average temp of fluid', val=np.random.uniform(300, 330), lower=1e-10, units='K', shape=(nn,)) + self.add_input("T_in", desc="Incoming coolant temp", units="K", shape=(nn,)) + self.add_input("q", desc="Heat INTO the fluid stream (positive is heat addition)", units="W", shape=(nn,)) + self.add_input("mdot_coolant", desc="Mass flow rate of coolant", units="kg/s", shape=(nn,)) + self.add_output( + "T_out", desc="Outgoing coolant temp", val=np.random.uniform(300, 330), lower=1e-10, units="K", shape=(nn,) + ) + self.add_output( + "T_average", + desc="Average temp of fluid", + val=np.random.uniform(300, 330), + lower=1e-10, + units="K", + shape=(nn,), + ) + + self.declare_partials(["T_out", "T_average"], ["q", "mdot_coolant"], rows=arange, cols=arange) + self.declare_partials("T_out", "T_in", rows=arange, cols=arange, val=np.ones((nn,))) + self.declare_partials("T_average", "T_in", rows=arange, cols=arange, val=np.ones((nn,))) - self.declare_partials(['T_out', 'T_average'], ['q', 'mdot_coolant'], rows=arange, cols=arange) - self.declare_partials('T_out', 'T_in', rows=arange, cols=arange, val=np.ones((nn,))) - self.declare_partials('T_average', 'T_in', rows=arange, cols=arange, val=np.ones((nn,))) - def compute(self, inputs, outputs): - outputs['T_out'] = inputs['T_in'] + inputs['q'] / self.options['specific_heat'] / inputs['mdot_coolant'] - outputs['T_average'] = (inputs['T_in'] + outputs['T_out']) / 2 - + outputs["T_out"] = inputs["T_in"] + inputs["q"] / self.options["specific_heat"] / inputs["mdot_coolant"] + outputs["T_average"] = (inputs["T_in"] + outputs["T_out"]) / 2 + def compute_partials(self, inputs, J): - J['T_out', 'q'] = 1 / self.options['specific_heat'] / inputs['mdot_coolant'] - J['T_out', 'mdot_coolant'] = - inputs['q'] / self.options['specific_heat'] / inputs['mdot_coolant']**2 + J["T_out", "q"] = 1 / self.options["specific_heat"] / inputs["mdot_coolant"] + J["T_out", "mdot_coolant"] = -inputs["q"] / self.options["specific_heat"] / inputs["mdot_coolant"] ** 2 + + J["T_average", "q"] = J["T_out", "q"] / 2 + J["T_average", "mdot_coolant"] = J["T_out", "mdot_coolant"] / 2 - J['T_average', 'q'] = J['T_out', 'q'] / 2 - J['T_average', 'mdot_coolant'] = J['T_out', 'mdot_coolant'] / 2 class ThermalComponentWithMass(ExplicitComponent): """ @@ -83,34 +95,36 @@ class ThermalComponentWithMass(ExplicitComponent): num_nodes : int The number of analysis points to run """ + def initialize(self): - self.options.declare('num_nodes', default=1) - self.options.declare('specific_heat', default=921, desc='Specific heat in J/kg/K - default 921 for aluminum') + self.options.declare("num_nodes", default=1) + self.options.declare("specific_heat", default=921, desc="Specific heat in J/kg/K - default 921 for aluminum") def setup(self): - nn_tot = self.options['num_nodes'] + nn_tot = self.options["num_nodes"] arange = np.arange(0, nn_tot) - self.add_input('q_in', units='W', shape=(nn_tot,)) - self.add_input('q_out', units='W', shape=(nn_tot,)) - self.add_input('mass', units='kg') - self.add_output('dTdt', units='K/s', shape=(nn_tot,)) + self.add_input("q_in", units="W", shape=(nn_tot,)) + self.add_input("q_out", units="W", shape=(nn_tot,)) + self.add_input("mass", units="kg") + self.add_output("dTdt", units="K/s", shape=(nn_tot,)) - self.declare_partials(['dTdt'], ['q_in'], rows=arange, cols=arange) - self.declare_partials(['dTdt'], ['q_out'], rows=arange, cols=arange) - self.declare_partials(['dTdt'], ['mass'], rows=arange, cols=np.zeros((nn_tot,))) + self.declare_partials(["dTdt"], ["q_in"], rows=arange, cols=arange) + self.declare_partials(["dTdt"], ["q_out"], rows=arange, cols=arange) + self.declare_partials(["dTdt"], ["mass"], rows=arange, cols=np.zeros((nn_tot,))) def compute(self, inputs, outputs): - spec_heat = self.options['specific_heat'] - outputs['dTdt'] = (inputs['q_in'] - inputs['q_out']) / inputs['mass'] / spec_heat + spec_heat = self.options["specific_heat"] + outputs["dTdt"] = (inputs["q_in"] - inputs["q_out"]) / inputs["mass"] / spec_heat def compute_partials(self, inputs, J): - nn_tot = self.options['num_nodes'] - spec_heat = self.options['specific_heat'] + nn_tot = self.options["num_nodes"] + spec_heat = self.options["specific_heat"] + + J["dTdt", "mass"] = -(inputs["q_in"] - inputs["q_out"]) / inputs["mass"] ** 2 / spec_heat + J["dTdt", "q_in"] = 1 / inputs["mass"] / spec_heat + J["dTdt", "q_out"] = -1 / inputs["mass"] / spec_heat - J['dTdt','mass'] = - (inputs['q_in'] - inputs['q_out']) / inputs['mass']**2 / spec_heat - J['dTdt','q_in'] = 1 / inputs['mass'] / spec_heat - J['dTdt','q_out'] = - 1 / inputs['mass'] / spec_heat class ThermalComponentMassless(ImplicitComponent): """ @@ -133,22 +147,24 @@ class ThermalComponentMassless(ImplicitComponent): num_nodes : int The number of analysis points to run """ + def initialize(self): - self.options.declare('num_nodes',default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn_tot = self.options['num_nodes'] + nn_tot = self.options["num_nodes"] arange = np.arange(0, nn_tot) - self.add_input('q_in', units='W', shape=(nn_tot,)) - self.add_input('q_out', units='W', shape=(nn_tot,)) - self.add_output('T_object', units='K', shape=(nn_tot,)) + self.add_input("q_in", units="W", shape=(nn_tot,)) + self.add_input("q_out", units="W", shape=(nn_tot,)) + self.add_output("T_object", units="K", shape=(nn_tot,)) - self.declare_partials(['T_object'], ['q_in'], rows=arange, cols=arange, val=np.ones((nn_tot,))) - self.declare_partials(['T_object'], ['q_out'], rows=arange, cols=arange, val=-np.ones((nn_tot,))) + self.declare_partials(["T_object"], ["q_in"], rows=arange, cols=arange, val=np.ones((nn_tot,))) + self.declare_partials(["T_object"], ["q_out"], rows=arange, cols=arange, val=-np.ones((nn_tot,))) def apply_nonlinear(self, inputs, outputs, residuals): - residuals['T_object'] = inputs['q_in'] - inputs['q_out'] + residuals["T_object"] = inputs["q_in"] - inputs["q_out"] + class ConstantSurfaceTemperatureColdPlate_NTU(ExplicitComponent): """ @@ -193,49 +209,61 @@ class ConstantSurfaceTemperatureColdPlate_NTU(ExplicitComponent): specific_heat : float Specific heat of the coolant (J/kg/K) (default 3801, glycol/water) """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Number of analysis points') - self.options.declare('fluid_rho', default=997.0, desc='Fluid density in kg/m3') - self.options.declare('fluid_k', default=0.405, desc='Thermal conductivity of the fluid in W / mK') - self.options.declare('nusselt', default=7.54, desc='Hydraulic diameter Nusselt number') - self.options.declare('specific_heat', default=3801, desc='Specific heat in J/kg/K') + self.options.declare("num_nodes", default=1, desc="Number of analysis points") + self.options.declare("fluid_rho", default=997.0, desc="Fluid density in kg/m3") + self.options.declare("fluid_k", default=0.405, desc="Thermal conductivity of the fluid in W / mK") + self.options.declare("nusselt", default=7.54, desc="Hydraulic diameter Nusselt number") + self.options.declare("specific_heat", default=3801, desc="Specific heat in J/kg/K") def setup(self): - nn_tot = self.options['num_nodes'] + nn_tot = self.options["num_nodes"] arange = np.arange(0, nn_tot) - self.add_input('T_in', units='K', shape=(nn_tot,)) - self.add_input('T_surface', units='K', shape=(nn_tot,)) - self.add_input('channel_width', units='m') - self.add_input('channel_height', units='m') - self.add_input('channel_length', units='m') - self.add_input('n_parallel') - self.add_input('mdot_coolant', units='kg/s', shape=(nn_tot,)) + self.add_input("T_in", units="K", shape=(nn_tot,)) + self.add_input("T_surface", units="K", shape=(nn_tot,)) + self.add_input("channel_width", units="m") + self.add_input("channel_height", units="m") + self.add_input("channel_length", units="m") + self.add_input("n_parallel") + self.add_input("mdot_coolant", units="kg/s", shape=(nn_tot,)) - self.add_output('q', units='W', shape=(nn_tot,)) - self.add_output('T_out', units='K', shape=(nn_tot,)) + self.add_output("q", units="W", shape=(nn_tot,)) + self.add_output("T_out", units="K", shape=(nn_tot,)) - self.declare_partials(['q','T_out'], ['T_in','T_surface','mdot_coolant'], method='cs') - self.declare_partials(['q','T_out'], ['channel_width','channel_height','channel_length','n_parallel'], method='cs') + self.declare_partials(["q", "T_out"], ["T_in", "T_surface", "mdot_coolant"], method="cs") + self.declare_partials( + ["q", "T_out"], ["channel_width", "channel_height", "channel_length", "n_parallel"], method="cs" + ) def compute(self, inputs, outputs): - Ts = inputs['T_surface'] - Ti = inputs['T_in'] - - Cmin = inputs['mdot_coolant'] * self.options['specific_heat'] - - #cross_section_area = inputs['channel_width'] * inputs['channel_height'] * inputs['n_parallel'] - #flow_rate = inputs['mdot_coolant'] / self.options['fluid_rho'] / cross_section_area # m/s - surface_area = 2 * (inputs['channel_width']*inputs['channel_length'] + - inputs['channel_height'] * inputs['channel_length']) * inputs['n_parallel'] - d_h = 2 * inputs['channel_width'] * inputs['channel_height'] / (inputs['channel_width'] + inputs['channel_height']) + Ts = inputs["T_surface"] + Ti = inputs["T_in"] + + Cmin = inputs["mdot_coolant"] * self.options["specific_heat"] + + # cross_section_area = inputs['channel_width'] * inputs['channel_height'] * inputs['n_parallel'] + # flow_rate = inputs['mdot_coolant'] / self.options['fluid_rho'] / cross_section_area # m/s + surface_area = ( + 2 + * (inputs["channel_width"] * inputs["channel_length"] + inputs["channel_height"] * inputs["channel_length"]) + * inputs["n_parallel"] + ) + d_h = ( + 2 + * inputs["channel_width"] + * inputs["channel_height"] + / (inputs["channel_width"] + inputs["channel_height"]) + ) # redh = self.options['fluid_rho'] * flow_rate * d_h / 3.39e-3 - h = self.options['nusselt'] * self.options['fluid_k'] / d_h + h = self.options["nusselt"] * self.options["fluid_k"] / d_h ntu = surface_area * h / Cmin effectiveness = 1 - np.exp(-ntu) - outputs['q'] = effectiveness * Cmin * (Ts - Ti) - outputs['T_out'] = inputs['T_in'] + outputs['q'] / inputs['mdot_coolant'] / self.options['specific_heat'] + outputs["q"] = effectiveness * Cmin * (Ts - Ti) + outputs["T_out"] = inputs["T_in"] + outputs["q"] / inputs["mdot_coolant"] / self.options["specific_heat"] + class LiquidCooledComp(Group): """A component (heat producing) with thermal mass @@ -284,33 +312,45 @@ class LiquidCooledComp(Group): """ def initialize(self): - self.options.declare('specific_heat_object', default=921.0, desc='Specific heat in J/kg/K') - self.options.declare('specific_heat_coolant', default=3801, desc='Specific heat in J/kg/K') - self.options.declare('quasi_steady', default=False, desc='Treat the component as quasi-steady or with thermal mass') - self.options.declare('num_nodes', default=1, desc='Number of quasi-steady points to runs') + self.options.declare("specific_heat_object", default=921.0, desc="Specific heat in J/kg/K") + self.options.declare("specific_heat_coolant", default=3801, desc="Specific heat in J/kg/K") + self.options.declare( + "quasi_steady", default=False, desc="Treat the component as quasi-steady or with thermal mass" + ) + self.options.declare("num_nodes", default=1, desc="Number of quasi-steady points to runs") def setup(self): - nn = self.options['num_nodes'] - quasi_steady = self.options['quasi_steady'] + nn = self.options["num_nodes"] + quasi_steady = self.options["quasi_steady"] if not quasi_steady: - self.add_subsystem('base', - ThermalComponentWithMass(specific_heat=self.options['specific_heat_object'], - num_nodes=nn), - promotes_inputs=['q_in', 'mass']) - ode_integ = self.add_subsystem('ode_integ', Integrator(num_nodes=nn, diff_units='s', method='simpson', time_setup='duration'), - promotes_outputs=['*'], promotes_inputs=['*']) - ode_integ.add_integrand('T', rate_name='dTdt', units='K', lower=1e-10) - self.connect('base.dTdt','dTdt') + self.add_subsystem( + "base", + ThermalComponentWithMass(specific_heat=self.options["specific_heat_object"], num_nodes=nn), + promotes_inputs=["q_in", "mass"], + ) + ode_integ = self.add_subsystem( + "ode_integ", + Integrator(num_nodes=nn, diff_units="s", method="simpson", time_setup="duration"), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + ode_integ.add_integrand("T", rate_name="dTdt", units="K", lower=1e-10) + self.connect("base.dTdt", "dTdt") else: - self.add_subsystem('base', - ThermalComponentMassless(num_nodes=nn), - promotes_inputs=['q_in'], - promotes_outputs=[('T_object', 'T')]) - self.add_subsystem('hex', - ConstantSurfaceTemperatureColdPlate_NTU(num_nodes=nn, specific_heat=self.options['specific_heat_coolant']), - promotes_inputs=['T_in', ('T_surface','T'),'n_parallel','channel*','mdot_coolant'], - promotes_outputs=['T_out']) - self.connect('hex.q','base.q_out') + self.add_subsystem( + "base", + ThermalComponentMassless(num_nodes=nn), + promotes_inputs=["q_in"], + promotes_outputs=[("T_object", "T")], + ) + self.add_subsystem( + "hex", + ConstantSurfaceTemperatureColdPlate_NTU(num_nodes=nn, specific_heat=self.options["specific_heat_coolant"]), + promotes_inputs=["T_in", ("T_surface", "T"), "n_parallel", "channel*", "mdot_coolant"], + promotes_outputs=["T_out"], + ) + self.connect("hex.q", "base.q_out") + class CoolantReservoir(Group): """A reservoir of coolant capable of buffering temperature @@ -342,18 +382,25 @@ class CoolantReservoir(Group): """ def initialize(self): - self.options.declare('num_nodes',default=5) + self.options.declare("num_nodes", default=5) def setup(self): - nn = self.options['num_nodes'] - self.add_subsystem('rate', - CoolantReservoirRate(num_nodes=nn), - promotes_inputs=['T_in', 'T_out', 'mass', 'mdot_coolant']) + nn = self.options["num_nodes"] + self.add_subsystem( + "rate", CoolantReservoirRate(num_nodes=nn), promotes_inputs=["T_in", "T_out", "mass", "mdot_coolant"] + ) + + ode_integ = self.add_subsystem( + "ode_integ", + Integrator(num_nodes=nn, diff_units="s", method="simpson", time_setup="duration"), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + ode_integ.add_integrand( + "T_out", rate_name="dTdt", start_name="T_initial", end_name="T_final", units="K", lower=1e-10 + ) + self.connect("rate.dTdt", "dTdt") - ode_integ = self.add_subsystem('ode_integ', Integrator(num_nodes=nn, diff_units='s', method='simpson', time_setup='duration'), - promotes_outputs=['*'], promotes_inputs=['*']) - ode_integ.add_integrand('T_out', rate_name='dTdt', start_name='T_initial', end_name='T_final', units='K', lower=1e-10) - self.connect('rate.dTdt','dTdt') class CoolantReservoirRate(ExplicitComponent): """ @@ -380,27 +427,28 @@ class CoolantReservoirRate(ExplicitComponent): num_nodes : int The number of analysis points to run """ + def initialize(self): - self.options.declare('num_nodes', default=1) + self.options.declare("num_nodes", default=1) def setup(self): - nn_tot = self.options['num_nodes'] + nn_tot = self.options["num_nodes"] arange = np.arange(0, nn_tot) - self.add_input('T_in', units='K', shape=(nn_tot,)) - self.add_input('T_out', units='K', shape=(nn_tot,)) - self.add_input('mdot_coolant', units='kg/s', shape=(nn_tot,)) - self.add_input('mass', units='kg') - self.add_output('dTdt', units='K/s', shape=(nn_tot,)) + self.add_input("T_in", units="K", shape=(nn_tot,)) + self.add_input("T_out", units="K", shape=(nn_tot,)) + self.add_input("mdot_coolant", units="kg/s", shape=(nn_tot,)) + self.add_input("mass", units="kg") + self.add_output("dTdt", units="K/s", shape=(nn_tot,)) - self.declare_partials(['dTdt'], ['T_in','T_out','mdot_coolant'], rows=arange, cols=arange) - self.declare_partials(['dTdt'], ['mass'], rows=arange, cols=np.zeros((nn_tot,))) + self.declare_partials(["dTdt"], ["T_in", "T_out", "mdot_coolant"], rows=arange, cols=arange) + self.declare_partials(["dTdt"], ["mass"], rows=arange, cols=np.zeros((nn_tot,))) def compute(self, inputs, outputs): - outputs['dTdt'] = inputs['mdot_coolant'] / inputs['mass'] * (inputs['T_in'] - inputs['T_out']) + outputs["dTdt"] = inputs["mdot_coolant"] / inputs["mass"] * (inputs["T_in"] - inputs["T_out"]) def compute_partials(self, inputs, J): - J['dTdt','mass'] = - inputs['mdot_coolant'] / inputs['mass']**2 * (inputs['T_in'] - inputs['T_out']) - J['dTdt','mdot_coolant'] = 1 / inputs['mass'] * (inputs['T_in'] - inputs['T_out']) - J['dTdt','T_in'] = inputs['mdot_coolant'] / inputs['mass'] - J['dTdt','T_out'] = - inputs['mdot_coolant'] / inputs['mass'] + J["dTdt", "mass"] = -inputs["mdot_coolant"] / inputs["mass"] ** 2 * (inputs["T_in"] - inputs["T_out"]) + J["dTdt", "mdot_coolant"] = 1 / inputs["mass"] * (inputs["T_in"] - inputs["T_out"]) + J["dTdt", "T_in"] = inputs["mdot_coolant"] / inputs["mass"] + J["dTdt", "T_out"] = -inputs["mdot_coolant"] / inputs["mass"] diff --git a/openconcept/utilities/dict_indepvarcomp.py b/openconcept/utilities/dict_indepvarcomp.py index 79cd88f6..925364a1 100644 --- a/openconcept/utilities/dict_indepvarcomp.py +++ b/openconcept/utilities/dict_indepvarcomp.py @@ -38,7 +38,7 @@ def __init__(self, data_dict, **kwargs): super(DictIndepVarComp, self).__init__(**kwargs) self._data_dict = data_dict - def add_output_from_dict(self, structured_name, separator='|', **kwargs): + def add_output_from_dict(self, structured_name, separator="|", **kwargs): """ Create a new output based on data from the data dictionary @@ -61,18 +61,18 @@ def add_output_from_dict(self, structured_name, separator='|', **kwargs): except KeyError: raise KeyError('"%s" does not exist in the data dictionary' % structured_name) try: - val = data_dict_tmp['value'] + val = data_dict_tmp["value"] except KeyError: raise KeyError('Data dict entry "%s" must have a "value" key' % structured_name) - units = data_dict_tmp.get('units', None) + units = data_dict_tmp.get("units", None) if isinstance(val, numbers.Number): val = np.array([val]) - super(DictIndepVarComp, self).add_output(name=structured_name, - val=val, units=units, shape=val.shape) + super(DictIndepVarComp, self).add_output(name=structured_name, val=val, units=units, shape=val.shape) -class DymosDesignParamsFromDict(): + +class DymosDesignParamsFromDict: r""" Create Dymos parameters from an external file with a Python dictionary. @@ -97,7 +97,7 @@ def __init__(self, data_dict, dymos_traj): self._data_dict = data_dict self._dymos_traj = dymos_traj - def add_output_from_dict(self, structured_name, separator='|', opt=False, dynamic=False, **kwargs): + def add_output_from_dict(self, structured_name, separator="|", opt=False, dynamic=False, **kwargs): """ Create a new output based on data from the data dictionary @@ -120,14 +120,16 @@ def add_output_from_dict(self, structured_name, separator='|', opt=False, dynami except KeyError: raise KeyError('"%s" does not exist in the data dictionary' % structured_name) try: - val = data_dict_tmp['value'] + val = data_dict_tmp["value"] except KeyError: raise KeyError('Data dict entry "%s" must have a "value" key' % structured_name) - units = data_dict_tmp.get('units', None) + units = data_dict_tmp.get("units", None) if isinstance(val, numbers.Number): val = np.array([val]) - targets = {phase : [structured_name] for phase in self._dymos_traj._phases.keys()} + targets = {phase: [structured_name] for phase in self._dymos_traj._phases.keys()} - self._dymos_traj.add_design_parameter(structured_name, units=units, val=val, opt=opt, targets=targets, dynamic=dynamic) \ No newline at end of file + self._dymos_traj.add_design_parameter( + structured_name, units=units, val=val, opt=opt, targets=targets, dynamic=dynamic + ) diff --git a/openconcept/utilities/dvlabel.py b/openconcept/utilities/dvlabel.py index 32a9108b..3d803e00 100644 --- a/openconcept/utilities/dvlabel.py +++ b/openconcept/utilities/dvlabel.py @@ -51,8 +51,7 @@ def setup(self): self.add_output(o_var, val, units=units) # partial derivs setup row_col = np.arange(size) - self.declare_partials(of=o_var, wrt=i_var, - val=np.ones(size), rows=row_col, cols=row_col) + self.declare_partials(of=o_var, wrt=i_var, val=np.ones(size), rows=row_col, cols=row_col) def compute(self, inputs, outputs): for var_list in self.vars_list: diff --git a/openconcept/utilities/linearinterp.py b/openconcept/utilities/linearinterp.py index 8380ddf0..3a0f31ba 100644 --- a/openconcept/utilities/linearinterp.py +++ b/openconcept/utilities/linearinterp.py @@ -3,7 +3,7 @@ class LinearInterpolator(ExplicitComponent): - ''' + """ Create a linearly interpolated set of points **including** two end points Inputs @@ -24,24 +24,22 @@ class LinearInterpolator(ExplicitComponent): Units for inputs and outputs num_nodes : int Number of linearly interpolated points to produce (minimum/default 2) - ''' + """ def initialize(self): - self.options.declare('num_nodes', default=2, desc="Number of nodes") - self.options.declare('units', default=None, desc='Units') + self.options.declare("num_nodes", default=2, desc="Number of nodes") + self.options.declare("units", default=None, desc="Units") def setup(self): - nn = self.options['num_nodes'] - units = self.options['units'] - self.add_input('start_val', units=units) - self.add_input('end_val', units=units) - self.add_output('vec', units=units, shape=(nn,)) + nn = self.options["num_nodes"] + units = self.options["units"] + self.add_input("start_val", units=units) + self.add_input("end_val", units=units) + self.add_output("vec", units=units, shape=(nn,)) arange = np.arange(0, nn) - self.declare_partials('vec', 'start_val', - rows=arange, cols=np.zeros(nn), val=np.linspace(1, 0, nn)) - self.declare_partials('vec', 'end_val', - rows=arange, cols=np.zeros(nn), val=np.linspace(0, 1, nn)) + self.declare_partials("vec", "start_val", rows=arange, cols=np.zeros(nn), val=np.linspace(1, 0, nn)) + self.declare_partials("vec", "end_val", rows=arange, cols=np.zeros(nn), val=np.linspace(0, 1, nn)) def compute(self, inputs, outputs): - nn = self.options['num_nodes'] - outputs['vec'] = np.linspace(inputs['start_val'], inputs['end_val'], nn).reshape((nn,)) + nn = self.options["num_nodes"] + outputs["vec"] = np.linspace(inputs["start_val"], inputs["end_val"], nn).reshape((nn,)) diff --git a/openconcept/utilities/math/add_subtract_comp.py b/openconcept/utilities/math/add_subtract_comp.py index edcec185..89c602d6 100644 --- a/openconcept/utilities/math/add_subtract_comp.py +++ b/openconcept/utilities/math/add_subtract_comp.py @@ -36,8 +36,9 @@ class AddSubtractComp(ExplicitComponent): List of equation systems to be initialized with the system. """ - def __init__(self, output_name=None, input_names=None, vec_size=1, length=1, - val=1.0, scaling_factors=None, **kwargs): + def __init__( + self, output_name=None, input_names=None, vec_size=1, length=1, val=1.0, scaling_factors=None, **kwargs + ): """ Allow user to create an addition/subtracton system with one-liner. @@ -74,23 +75,36 @@ def __init__(self, output_name=None, input_names=None, vec_size=1, length=1, self._add_systems = [] if isinstance(output_name, str): - self._add_systems.append((output_name, input_names, vec_size, length, val, - scaling_factors, kwargs)) + self._add_systems.append((output_name, input_names, vec_size, length, val, scaling_factors, kwargs)) elif isinstance(output_name, Iterable): - raise NotImplementedError('Declaring multiple addition systems ' - 'on initiation is not implemented.' - 'Use a string to name a single addition relationship or use ' - 'multiple add_output calls') + raise NotImplementedError( + "Declaring multiple addition systems " + "on initiation is not implemented." + "Use a string to name a single addition relationship or use " + "multiple add_output calls" + ) elif output_name is None: pass else: - raise ValueError( - "first argument to adder init must be either of type " - "'str' or 'None'") - - def add_equation(self, output_name, input_names, vec_size=1, length=1, val=1.0, - units=None, res_units=None, desc='', lower=None, upper=None, ref=1.0, - ref0=0.0, res_ref=None, scaling_factors=None): + raise ValueError("first argument to adder init must be either of type " "'str' or 'None'") + + def add_equation( + self, + output_name, + input_names, + vec_size=1, + length=1, + val=1.0, + units=None, + res_units=None, + desc="", + lower=None, + upper=None, + ref=1.0, + ref0=0.0, + res_ref=None, + scaling_factors=None, + ): """ Add an addition/subtraction relation. @@ -145,25 +159,31 @@ def add_equation(self, output_name, input_names, vec_size=1, length=1, val=1.0, Scaling parameter. The value in the user-defined res_units of this output's residual when the scaled value is 1. Default is 1. """ - kwargs = {'units': units, 'res_units': res_units, 'desc': desc, - 'lower': lower, 'upper': upper, 'ref': ref, 'ref0': ref0, - 'res_ref': res_ref} - self._add_systems.append((output_name, input_names, vec_size, length, val, - scaling_factors, kwargs)) + kwargs = { + "units": units, + "res_units": res_units, + "desc": desc, + "lower": lower, + "upper": upper, + "ref": ref, + "ref0": ref0, + "res_ref": res_ref, + } + self._add_systems.append((output_name, input_names, vec_size, length, val, scaling_factors, kwargs)) def add_output(self): """ Use add_equation instead of add_output to define equation systems. """ - raise NotImplementedError('Use add_equation method, not add_output method' - 'to create an addition/subtraction relation') + raise NotImplementedError( + "Use add_equation method, not add_output method" "to create an addition/subtraction relation" + ) def setup(self): """ Set up the addition/subtraction system at run time. """ - for (output_name, input_names, vec_size, length, val, - scaling_factors, kwargs) in self._add_systems: + for (output_name, input_names, vec_size, length, val, scaling_factors, kwargs) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] @@ -171,13 +191,13 @@ def setup(self): scaling_factors = np.ones(len(input_names)) if len(scaling_factors) != len(input_names): - raise ValueError('Scaling factors list needs to be same length as input names') + raise ValueError("Scaling factors list needs to be same length as input names") if isinstance(vec_size, Iterable): # scalar - vector mutliplication multi_vec_size = True if len(vec_size) != len(input_names): - raise ValueError('Inputs list needs to be same length as vec_sizes list') + raise ValueError("Inputs list needs to be same length as vec_sizes list") vec_out_size = max(vec_size) else: multi_vec_size = False @@ -189,8 +209,8 @@ def setup(self): out_shape = (vec_out_size, length) super(AddSubtractComp, self).add_output(output_name, val, shape=out_shape, **kwargs) - units = kwargs.pop('units', None) - desc = kwargs.pop('desc', '') + units = kwargs.pop("units", None) + desc = kwargs.pop("desc", "") for i, input_name in enumerate(input_names): if multi_vec_size: @@ -209,13 +229,15 @@ def setup(self): else: # vector input col_vals = np.arange(0, vec_out_size * length) - self.add_input(input_name, shape=shape, units=units, - desc=desc + '_inp_' + input_name) + self.add_input(input_name, shape=shape, units=units, desc=desc + "_inp_" + input_name) sf = scaling_factors[i] - self.declare_partials([output_name], [input_name], - cols=col_vals, - rows=np.arange(0, vec_out_size * length), - val=sf * np.ones((vec_out_size * length))) + self.declare_partials( + [output_name], + [input_name], + cols=col_vals, + rows=np.arange(0, vec_out_size * length), + val=sf * np.ones((vec_out_size * length)), + ) def compute(self, inputs, outputs): """ @@ -228,8 +250,7 @@ def compute(self, inputs, outputs): outputs : Vector unscaled, dimensional output variables read via outputs[key] """ - for (output_name, input_names, vec_size, length, val, scaling_factors, - kwargs) in self._add_systems: + for (output_name, input_names, vec_size, length, val, scaling_factors, kwargs) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] diff --git a/openconcept/utilities/math/combine_split_comp.py b/openconcept/utilities/math/combine_split_comp.py index a7f6e6c4..e6948efa 100644 --- a/openconcept/utilities/math/combine_split_comp.py +++ b/openconcept/utilities/math/combine_split_comp.py @@ -25,8 +25,7 @@ class VectorConcatenateComp(ExplicitComponent): List of equation systems to be initialized with the system. """ - def __init__(self, output_name=None, input_names=None, vec_sizes=None, length=1, - val=1.0, **kwargs): + def __init__(self, output_name=None, input_names=None, vec_sizes=None, length=1, val=1.0, **kwargs): """ Allow user to create an addition/subtracton system with one-liner. @@ -54,28 +53,38 @@ def __init__(self, output_name=None, input_names=None, vec_sizes=None, length=1, self._add_systems = [] if isinstance(output_name, str): - if (not isinstance(input_names, Iterable) or - not isinstance(vec_sizes, Iterable)): - raise ValueError('User must provide list of input name(s)' - 'and list of vec_sizes for each input') + if not isinstance(input_names, Iterable) or not isinstance(vec_sizes, Iterable): + raise ValueError("User must provide list of input name(s)" "and list of vec_sizes for each input") - self._add_systems.append((output_name, input_names, vec_sizes, length, val, - kwargs)) + self._add_systems.append((output_name, input_names, vec_sizes, length, val, kwargs)) elif isinstance(output_name, Iterable): - raise NotImplementedError('Declaring multiple relations ' - 'on initiation is not implemented.' - 'Use a string to name a single addition relationship or use ' - 'multiple add_relation calls') + raise NotImplementedError( + "Declaring multiple relations " + "on initiation is not implemented." + "Use a string to name a single addition relationship or use " + "multiple add_relation calls" + ) elif output_name is None: pass else: - raise ValueError( - "First argument to init must be either of type " - "'str' or 'None'") - - def add_relation(self, output_name, input_names, vec_sizes, length=1, val=1.0, - units=None, res_units=None, desc='', lower=None, upper=None, ref=1.0, - ref0=0.0, res_ref=None): + raise ValueError("First argument to init must be either of type " "'str' or 'None'") + + def add_relation( + self, + output_name, + input_names, + vec_sizes, + length=1, + val=1.0, + units=None, + res_units=None, + desc="", + lower=None, + upper=None, + ref=1.0, + ref0=0.0, + res_ref=None, + ): """ Add a concatenation relation. @@ -121,66 +130,69 @@ def add_relation(self, output_name, input_names, vec_sizes, length=1, val=1.0, Scaling parameter. The value in the user-defined res_units of this output's residual when the scaled value is 1. Default is 1. """ - kwargs = {'units': units, 'res_units': res_units, 'desc': desc, - 'lower': lower, 'upper': upper, 'ref': ref, 'ref0': ref0, - 'res_ref': res_ref} - - if (not isinstance(input_names, Iterable) or - not isinstance(vec_sizes, Iterable)): - raise ValueError('User must provide list of input name(s)' - 'and list of vec_sizes for each input') - - self._add_systems.append((output_name, input_names, vec_sizes, length, val, - kwargs)) + kwargs = { + "units": units, + "res_units": res_units, + "desc": desc, + "lower": lower, + "upper": upper, + "ref": ref, + "ref0": ref0, + "res_ref": res_ref, + } + + if not isinstance(input_names, Iterable) or not isinstance(vec_sizes, Iterable): + raise ValueError("User must provide list of input name(s)" "and list of vec_sizes for each input") + + self._add_systems.append((output_name, input_names, vec_sizes, length, val, kwargs)) def add_output(self): """ Use add_relation instead of add_output to define concatenate relations. """ - raise NotImplementedError('Use add_relation method, not add_output method' - 'to create an concatenate relation') + raise NotImplementedError("Use add_relation method, not add_output method" "to create an concatenate relation") def setup(self): """ Set up the component at run time from both add_relation and __init__. """ - for (output_name, input_names, vec_sizes, length, val, - kwargs) in self._add_systems: + for (output_name, input_names, vec_sizes, length, val, kwargs) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] - units = kwargs.get('units', None) - desc = kwargs.get('desc', '') + units = kwargs.get("units", None) + desc = kwargs.get("desc", "") if len(vec_sizes) != len(input_names): - raise ValueError('vec_sizes list needs to be same length as input names list') + raise ValueError("vec_sizes list needs to be same length as input names list") output_size = np.sum(vec_sizes) if length == 1: output_shape = (output_size,) else: output_shape = (output_size, length) - super(VectorConcatenateComp, self).add_output(output_name, val, - shape=output_shape, **kwargs) + super(VectorConcatenateComp, self).add_output(output_name, val, shape=output_shape, **kwargs) for i, input_name in enumerate(input_names): if length == 1: input_shape = (vec_sizes[i],) else: input_shape = (vec_sizes[i], length) - self.add_input(input_name, shape=input_shape, units=units, - desc=desc + '_inp_' + input_name) + self.add_input(input_name, shape=input_shape, units=units, desc=desc + "_inp_" + input_name) if i == 0: start_idx = 0 else: start_idx = np.sum(vec_sizes[0:i]) - end_idx = np.sum(vec_sizes[0:i + 1]) + end_idx = np.sum(vec_sizes[0 : i + 1]) rowidxs = np.arange(start_idx * length, end_idx * length) - self.declare_partials([output_name], [input_name], - rows=rowidxs, - cols=np.arange(0, vec_sizes[i] * length), - val=np.ones(vec_sizes[i] * length)) + self.declare_partials( + [output_name], + [input_name], + rows=rowidxs, + cols=np.arange(0, vec_sizes[i] * length), + val=np.ones(vec_sizes[i] * length), + ) def compute(self, inputs, outputs): """ @@ -193,8 +205,7 @@ def compute(self, inputs, outputs): outputs : Vector unscaled, dimensional output variables read via outputs[key] """ - for (output_name, input_names, vec_sizes, length, val, - kwargs) in self._add_systems: + for (output_name, input_names, vec_sizes, length, val, kwargs) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] @@ -233,8 +244,7 @@ class VectorSplitComp(ExplicitComponent): List of equation systems to be initialized with the system. """ - def __init__(self, output_names=None, input_name=None, vec_sizes=None, length=1, - val=1.0, **kwargs): + def __init__(self, output_names=None, input_name=None, vec_sizes=None, length=1, val=1.0, **kwargs): """ Allow user to create an addition/subtracton system with one-liner. @@ -262,28 +272,38 @@ def __init__(self, output_names=None, input_name=None, vec_sizes=None, length=1, self._add_systems = [] if isinstance(input_name, str): - if (not isinstance(output_names, Iterable) or - not isinstance(vec_sizes, Iterable)): - raise ValueError('User must provide list of output name(s)' - 'and list of vec_sizes for each input') + if not isinstance(output_names, Iterable) or not isinstance(vec_sizes, Iterable): + raise ValueError("User must provide list of output name(s)" "and list of vec_sizes for each input") - self._add_systems.append((output_names, input_name, vec_sizes, length, val, - kwargs)) + self._add_systems.append((output_names, input_name, vec_sizes, length, val, kwargs)) elif isinstance(input_name, Iterable): - raise NotImplementedError('Declaring multiple relations ' - 'on initiation is not implemented.' - 'Use a string to name a single addition relationship or use ' - 'multiple add_relation calls') + raise NotImplementedError( + "Declaring multiple relations " + "on initiation is not implemented." + "Use a string to name a single addition relationship or use " + "multiple add_relation calls" + ) elif input_name is None: pass else: - raise ValueError( - "input_name argument to init must be either of type " - "'str' or 'None'") - - def add_relation(self, output_names, input_name, vec_sizes, length=1, val=1.0, - units=None, res_units=None, desc='', lower=None, upper=None, ref=1.0, - ref0=0.0, res_ref=None): + raise ValueError("input_name argument to init must be either of type " "'str' or 'None'") + + def add_relation( + self, + output_names, + input_name, + vec_sizes, + length=1, + val=1.0, + units=None, + res_units=None, + desc="", + lower=None, + upper=None, + ref=1.0, + ref0=0.0, + res_ref=None, + ): """ Add a concatenation relation. @@ -329,65 +349,68 @@ def add_relation(self, output_names, input_name, vec_sizes, length=1, val=1.0, Scaling parameter. The value in the user-defined res_units of this output's residual when the scaled value is 1. Default is 1. """ - kwargs = {'units': units, 'res_units': res_units, 'desc': desc, - 'lower': lower, 'upper': upper, 'ref': ref, 'ref0': ref0, - 'res_ref': res_ref} - - if (not isinstance(output_names, Iterable) or - not isinstance(vec_sizes, Iterable)): - raise ValueError('User must provide list of output name(s)' - 'and list of vec_sizes for each input') - - self._add_systems.append((output_names, input_name, vec_sizes, length, val, - kwargs)) + kwargs = { + "units": units, + "res_units": res_units, + "desc": desc, + "lower": lower, + "upper": upper, + "ref": ref, + "ref0": ref0, + "res_ref": res_ref, + } + + if not isinstance(output_names, Iterable) or not isinstance(vec_sizes, Iterable): + raise ValueError("User must provide list of output name(s)" "and list of vec_sizes for each input") + + self._add_systems.append((output_names, input_name, vec_sizes, length, val, kwargs)) def add_output(self): """ Use add_relation instead of add_output to define split relations. """ - raise NotImplementedError('Use add_relation method, not add_output method' - 'to create a split relation') + raise NotImplementedError("Use add_relation method, not add_output method" "to create a split relation") def setup(self): """ Set up the component at run time from both add_relation and __init__. """ - for (output_names, input_name, vec_sizes, length, val, - kwargs) in self._add_systems: + for (output_names, input_name, vec_sizes, length, val, kwargs) in self._add_systems: if isinstance(output_names, str): output_names = [output_names] - units = kwargs.get('units', None) - desc = kwargs.get('desc', '') + units = kwargs.get("units", None) + desc = kwargs.get("desc", "") if len(vec_sizes) != len(output_names): - raise ValueError('vec_sizes list needs to be same length as output names list') + raise ValueError("vec_sizes list needs to be same length as output names list") input_size = np.sum(vec_sizes) if length == 1: input_shape = (input_size,) else: input_shape = (input_size, length) - self.add_input(input_name, shape=input_shape, units=units, - desc=desc + '_inp_' + input_name) + self.add_input(input_name, shape=input_shape, units=units, desc=desc + "_inp_" + input_name) for i, output_name in enumerate(output_names): if length == 1: output_shape = (vec_sizes[i],) else: output_shape = (vec_sizes[i], length) - super(VectorSplitComp, self).add_output(output_name, val, - shape=output_shape, **kwargs) + super(VectorSplitComp, self).add_output(output_name, val, shape=output_shape, **kwargs) if i == 0: start_idx = 0 else: start_idx = np.sum(vec_sizes[0:i]) - end_idx = np.sum(vec_sizes[0:i + 1]) + end_idx = np.sum(vec_sizes[0 : i + 1]) colidx = np.arange(start_idx * length, end_idx * length) - self.declare_partials([output_name], [input_name], - rows=np.arange(0, vec_sizes[i] * length), - cols=colidx, - val=np.ones(vec_sizes[i] * length)) + self.declare_partials( + [output_name], + [input_name], + rows=np.arange(0, vec_sizes[i] * length), + cols=colidx, + val=np.ones(vec_sizes[i] * length), + ) def compute(self, inputs, outputs): """ @@ -400,8 +423,7 @@ def compute(self, inputs, outputs): outputs : Vector unscaled, dimensional output variables read via outputs[key] """ - for (output_names, input_name, vec_sizes, length, val, - kwargs) in self._add_systems: + for (output_names, input_name, vec_sizes, length, val, kwargs) in self._add_systems: if isinstance(output_names, str): output_names = [output_names] @@ -415,7 +437,7 @@ def compute(self, inputs, outputs): start_idx = 0 else: start_idx = np.sum(vec_sizes[0:i]) - end_idx = np.sum(vec_sizes[0:i + 1]) + end_idx = np.sum(vec_sizes[0 : i + 1]) if length == 1: outputs[output_name] = inputs[input_name][start_idx:end_idx] else: diff --git a/openconcept/utilities/math/derivatives.py b/openconcept/utilities/math/derivatives.py index 8cd5b061..62cd2352 100644 --- a/openconcept/utilities/math/derivatives.py +++ b/openconcept/utilities/math/derivatives.py @@ -3,49 +3,51 @@ from openmdao.api import ExplicitComponent import warnings + def first_deriv_second_order_accurate_stencil(nn_seg): # thought: eventually, create this matrix upon initializing the component to save time # assemble a sparse matrix which includes the finite difference stencil for ONE segment # CSR format: mat[rowidx[i], colidx[i]] = matvec[i] # TODO: middle CD coefficient is 0, could remove from sparsity pattern for big improvement - rowidx = np.repeat(np.arange(1, nn_seg-1), 2) # [1, 1, 2, 2, .... ] - rowidx = np.concatenate([np.tile(0, 3), rowidx, np.tile(nn_seg-1, 3)]) # adds one-sided [0,0,0,1,1,2,2...] + rowidx = np.repeat(np.arange(1, nn_seg - 1), 2) # [1, 1, 2, 2, .... ] + rowidx = np.concatenate([np.tile(0, 3), rowidx, np.tile(nn_seg - 1, 3)]) # adds one-sided [0,0,0,1,1,2,2...] # the columns have an interesting sparsity pattern due to the one sided stencils at the edges: # [0, 1, 2, 0, 2, 1, 3, 2, 4, ...] - offset = np.repeat(np.arange(0, nn_seg-2), 2) - colidx = np.tile(np.array([0, 2]), nn_seg-2) + offset - colidx = np.concatenate([np.arange(0,3), colidx, np.arange(nn_seg-3,nn_seg)]) + offset = np.repeat(np.arange(0, nn_seg - 2), 2) + colidx = np.tile(np.array([0, 2]), nn_seg - 2) + offset + colidx = np.concatenate([np.arange(0, 3), colidx, np.arange(nn_seg - 3, nn_seg)]) # the central difference stencil is: - cd_stencil = np.array([-1/2, 1/2]) + cd_stencil = np.array([-1 / 2, 1 / 2]) # the biased stencils for the first, second, second-to-last, and last entries are: - fwd_0_stencil = np.array([-3/2, 2, -1/2]) - bwd_0_stencil = np.array([1/2, -2, 3/2]) + fwd_0_stencil = np.array([-3 / 2, 2, -1 / 2]) + bwd_0_stencil = np.array([1 / 2, -2, 3 / 2]) stencil_vec = np.tile(cd_stencil, nn_seg - 2) stencil_vec = np.concatenate([fwd_0_stencil, stencil_vec, bwd_0_stencil]) return stencil_vec, rowidx, colidx + def first_deriv_fourth_order_accurate_stencil(nn_seg): # thought: eventually, create this matrix upon initializing the component to save time # assemble a sparse matrix which includes the finite difference stencil for ONE segment # CSR format: mat[rowidx[i], colidx[i]] = matvec[i] - rowidx = np.repeat(np.arange(0, nn_seg), 5) # [0, 0, 0, 0, 0, 1, 1, 1, 1, 1 .... ] + rowidx = np.repeat(np.arange(0, nn_seg), 5) # [0, 0, 0, 0, 0, 1, 1, 1, 1, 1 .... ] # the columns have an interesting sparsity pattern due to the one sided stencils at the edges: # [0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 1, 2, 3, 4, 5, 2, 3, 4, 5, 6, ...] - offset = np.repeat(np.arange(-2, nn_seg-2), 5) - colidx = np.tile(np.arange(0,5), nn_seg) + offset - colidx[:10] = np.tile(np.arange(0,5), 2) - colidx[-10:] = np.tile(np.arange(nn_seg-5, nn_seg), 2) + offset = np.repeat(np.arange(-2, nn_seg - 2), 5) + colidx = np.tile(np.arange(0, 5), nn_seg) + offset + colidx[:10] = np.tile(np.arange(0, 5), 2) + colidx[-10:] = np.tile(np.arange(nn_seg - 5, nn_seg), 2) # the central difference stencil is: - cd_stencil = np.array([1/12, -2/3, 0, 2/3, -1/12]) + cd_stencil = np.array([1 / 12, -2 / 3, 0, 2 / 3, -1 / 12]) # the biased stencils for the first, second, second-to-last, and last entries are: - fwd_0_stencil = np.array([-25/12, 4, -3, 4/3, -1/4]) - fwd_1_stencil = np.array([-1/4, -5/6, 3/2, -1/2, 1/12]) - bwd_1_stencil = np.array([-1/12, 1/2, -3/2, 5/6, 1/4]) - bwd_0_stencil = np.array([1/4, -4/3, 3, -4, 25/12]) + fwd_0_stencil = np.array([-25 / 12, 4, -3, 4 / 3, -1 / 4]) + fwd_1_stencil = np.array([-1 / 4, -5 / 6, 3 / 2, -1 / 2, 1 / 12]) + bwd_1_stencil = np.array([-1 / 12, 1 / 2, -3 / 2, 5 / 6, 1 / 4]) + bwd_0_stencil = np.array([1 / 4, -4 / 3, 3, -4, 25 / 12]) stencil_vec = np.tile(cd_stencil, nn_seg) stencil_vec[:5] = fwd_0_stencil stencil_vec[5:10] = fwd_1_stencil @@ -54,6 +56,7 @@ def first_deriv_fourth_order_accurate_stencil(nn_seg): return stencil_vec, rowidx, colidx + def first_deriv(dts, q, n_segments=1, n_simpson_intervals_per_segment=2, order=4): """ This method differentiates a quantity over time using fourth order finite differencing @@ -87,14 +90,16 @@ def first_deriv(dts, q, n_segments=1, n_simpson_intervals_per_segment=2, order=4 n_int_seg = n_simpson_intervals_per_segment n_int_tot = n_segments * n_int_seg - nn_seg = (n_simpson_intervals_per_segment * 2 + 1) + nn_seg = n_simpson_intervals_per_segment * 2 + 1 nn_tot = n_segments * nn_seg if order == 4 and n_int_seg < 2: - raise ValueError('Must use a minimum of 2 Simpson intervals or 5 points per segment due to fourth-order FD stencil') + raise ValueError( + "Must use a minimum of 2 Simpson intervals or 5 points per segment due to fourth-order FD stencil" + ) if len(q) != nn_tot: - raise ValueError('q must be of the correct length') + raise ValueError("q must be of the correct length") if len(dts) != n_segments: - raise ValueError('must provide same number of dts as segments') + raise ValueError("must provide same number of dts as segments") dqdt = np.zeros(q.shape) if order == 4: @@ -102,7 +107,7 @@ def first_deriv(dts, q, n_segments=1, n_simpson_intervals_per_segment=2, order=4 elif order == 2: stencil_vec, rowidx, colidx = first_deriv_second_order_accurate_stencil(nn_seg) else: - raise ValueError('Must choose second or fourth order accuracy') + raise ValueError("Must choose second or fourth order accuracy") stencil_mat = sp.csr_matrix((stencil_vec, (rowidx, colidx))) # now we have a generic stencil for each segment @@ -116,6 +121,7 @@ def first_deriv(dts, q, n_segments=1, n_simpson_intervals_per_segment=2, order=4 dqdt = overall_stencil.dot(q) return dqdt + def first_deriv_partials(dts, q, n_segments=1, n_simpson_intervals_per_segment=2, order=4): """ This method provides the Jacobian of a temporal first derivative @@ -150,14 +156,16 @@ def first_deriv_partials(dts, q, n_segments=1, n_simpson_intervals_per_segment=2 """ n_int_seg = n_simpson_intervals_per_segment n_int_tot = n_segments * n_int_seg - nn_seg = (n_simpson_intervals_per_segment * 2 + 1) + nn_seg = n_simpson_intervals_per_segment * 2 + 1 nn_tot = n_segments * nn_seg if order == 4 and n_int_seg < 2: - raise ValueError('Must use a minimum of 2 Simpson intervals or 5 points per segment due to fourth-order FD stencil') + raise ValueError( + "Must use a minimum of 2 Simpson intervals or 5 points per segment due to fourth-order FD stencil" + ) if len(q) != nn_tot: - raise ValueError('q must be of the correct length') + raise ValueError("q must be of the correct length") if len(dts) != n_segments: - raise ValueError('must provide same number of dts as segments') + raise ValueError("must provide same number of dts as segments") dqdt = np.zeros(q.shape) if order == 4: @@ -165,7 +173,7 @@ def first_deriv_partials(dts, q, n_segments=1, n_simpson_intervals_per_segment=2 elif order == 2: stencil_vec, rowidx, colidx = first_deriv_second_order_accurate_stencil(nn_seg) else: - raise ValueError('Must choose second or fourth order accuracy') + raise ValueError("Must choose second or fourth order accuracy") # now we have a generic stencil for each segment @@ -195,13 +203,14 @@ def first_deriv_partials(dts, q, n_segments=1, n_simpson_intervals_per_segment=2 n_to_tile = 5 rowidxs_wrt_dt.append(np.arange(0, nn_seg) + i * nn_seg) colidxs_wrt_dt.append(np.zeros((nn_seg,), dtype=np.int32)) - local_partials = - np.dot(stencil_mat, q[i * nn_seg: (i + 1) * nn_seg]) * dt_seg ** -2 + local_partials = -np.dot(stencil_mat, q[i * nn_seg : (i + 1) * nn_seg]) * dt_seg**-2 partials_wrt_dt.append(local_partials) wrt_q = [rowidx_wrt_q.astype(np.int32), colidx_wrt_q.astype(np.int32), partials_wrt_q] wrt_dt = [rowidxs_wrt_dt, colidxs_wrt_dt, partials_wrt_dt] return wrt_q, wrt_dt + class FirstDerivative(ExplicitComponent): """ This component differentiates a vector using a second or fourth order finite difference approximation @@ -215,6 +224,7 @@ class FirstDerivative(ExplicitComponent): q is of length nn_tot where nn_tot = n_segments x(2 x num_intervals + 1) Each segment of q is of length nn_seg = (2 x num_intervals + 1) """ + """ For example, a two-interval vector q has indices |0 1 2 3 4 | 5 6 7 8 9 | Elements 4 and 5 correspond to exactly the same time point @@ -248,20 +258,20 @@ class FirstDerivative(ExplicitComponent): """ def initialize(self): - self.options.declare('segment_names', default=None, desc="Names of differentiation segments") - self.options.declare('quantity_units',default=None, desc="Units of the quantity being differentiated") - self.options.declare('diff_units',default=None, desc="Units of the differential") - self.options.declare('num_intervals',default=5, desc="Number of Simpsons rule intervals per segment") - self.options.declare('order',default=4, desc="Order of accuracy") + self.options.declare("segment_names", default=None, desc="Names of differentiation segments") + self.options.declare("quantity_units", default=None, desc="Units of the quantity being differentiated") + self.options.declare("diff_units", default=None, desc="Units of the differential") + self.options.declare("num_intervals", default=5, desc="Number of Simpsons rule intervals per segment") + self.options.declare("order", default=4, desc="Order of accuracy") def setup(self): - segment_names = self.options['segment_names'] - quantity_units = self.options['quantity_units'] - diff_units = self.options['diff_units'] - order = self.options['order'] + segment_names = self.options["segment_names"] + quantity_units = self.options["quantity_units"] + diff_units = self.options["diff_units"] + order = self.options["order"] - n_int_per_seg = self.options['num_intervals'] - nn_seg = (n_int_per_seg*2 + 1) + n_int_per_seg = self.options["num_intervals"] + nn_seg = n_int_per_seg * 2 + 1 if segment_names is None: n_segments = 1 else: @@ -271,70 +281,79 @@ def setup(self): if quantity_units is None and diff_units is None: deriv_units = None elif quantity_units is None: - deriv_units = '(' + diff_units +')** -1' + deriv_units = "(" + diff_units + ")** -1" elif diff_units is None: deriv_units = quantity_units - warnings.warn('You have specified a derivative with respect to a unitless differential. Be aware of this.') + warnings.warn("You have specified a derivative with respect to a unitless differential. Be aware of this.") else: - deriv_units = '('+quantity_units+') / (' + diff_units +')' - wrt_q, wrt_dt = first_deriv_partials(np.ones((n_segments, )), np.ones((nn_tot,)), n_segments=n_segments, n_simpson_intervals_per_segment=n_int_per_seg, order=order) - - self.add_input('q', val=0, units=quantity_units, desc='Quantity to differentiate',shape=(nn_tot,)) - self.add_output('dqdt', units=deriv_units, desc='First derivative of q', shape=(nn_tot,)) - self.declare_partials(['dqdt'], ['q'], rows=wrt_q[0], cols=wrt_q[1]) - + deriv_units = "(" + quantity_units + ") / (" + diff_units + ")" + wrt_q, wrt_dt = first_deriv_partials( + np.ones((n_segments,)), + np.ones((nn_tot,)), + n_segments=n_segments, + n_simpson_intervals_per_segment=n_int_per_seg, + order=order, + ) + + self.add_input("q", val=0, units=quantity_units, desc="Quantity to differentiate", shape=(nn_tot,)) + self.add_output("dqdt", units=deriv_units, desc="First derivative of q", shape=(nn_tot,)) + self.declare_partials(["dqdt"], ["q"], rows=wrt_q[0], cols=wrt_q[1]) if segment_names is None: - self.add_input('dt', units=diff_units, desc='Time step') - self.declare_partials(['dqdt'], ['dt'], rows=wrt_dt[0][0], cols=wrt_dt[1][0]) + self.add_input("dt", units=diff_units, desc="Time step") + self.declare_partials(["dqdt"], ["dt"], rows=wrt_dt[0][0], cols=wrt_dt[1][0]) else: for i_seg, segment_name in enumerate(segment_names): - self.add_input(segment_name +'|dt', units=diff_units, desc='Time step') - self.declare_partials(['dqdt'], [segment_name +'|dt'], rows=wrt_dt[0][i_seg], cols=wrt_dt[1][i_seg]) + self.add_input(segment_name + "|dt", units=diff_units, desc="Time step") + self.declare_partials(["dqdt"], [segment_name + "|dt"], rows=wrt_dt[0][i_seg], cols=wrt_dt[1][i_seg]) def compute(self, inputs, outputs): - segment_names = self.options['segment_names'] - quantity_units = self.options['quantity_units'] - diff_units = self.options['diff_units'] - order = self.options['order'] - n_int_per_seg = self.options['num_intervals'] - nn_seg = (n_int_per_seg*2 + 1) + segment_names = self.options["segment_names"] + quantity_units = self.options["quantity_units"] + diff_units = self.options["diff_units"] + order = self.options["order"] + n_int_per_seg = self.options["num_intervals"] + nn_seg = n_int_per_seg * 2 + 1 if segment_names is None: n_segments = 1 - dts = [inputs['dt'][0]] + dts = [inputs["dt"][0]] else: n_segments = len(segment_names) dts = [] for i_seg, segment_name in enumerate(segment_names): - input_name = segment_name+'|dt' + input_name = segment_name + "|dt" dts.append(inputs[input_name][0]) nn_tot = nn_seg * n_segments - dqdt = first_deriv(dts, inputs['q'], n_segments=n_segments, n_simpson_intervals_per_segment=n_int_per_seg, order=order) - outputs['dqdt'] = dqdt + dqdt = first_deriv( + dts, inputs["q"], n_segments=n_segments, n_simpson_intervals_per_segment=n_int_per_seg, order=order + ) + outputs["dqdt"] = dqdt def compute_partials(self, inputs, J): - segment_names = self.options['segment_names'] - quantity_units = self.options['quantity_units'] - diff_units = self.options['diff_units'] - order = self.options['order'] + segment_names = self.options["segment_names"] + quantity_units = self.options["quantity_units"] + diff_units = self.options["diff_units"] + order = self.options["order"] - n_int_per_seg = self.options['num_intervals'] - nn_seg = (n_int_per_seg*2 + 1) + n_int_per_seg = self.options["num_intervals"] + nn_seg = n_int_per_seg * 2 + 1 if segment_names is None: n_segments = 1 - dts = [inputs['dt'][0]] + dts = [inputs["dt"][0]] else: n_segments = len(segment_names) dts = [] for i_seg, segment_name in enumerate(segment_names): - input_name = segment_name+'|dt' + input_name = segment_name + "|dt" dts.append(inputs[input_name][0]) nn_tot = nn_seg * n_segments - wrt_q, wrt_dt = first_deriv_partials(dts, inputs['q'], n_segments=n_segments, n_simpson_intervals_per_segment=n_int_per_seg, order=order) + wrt_q, wrt_dt = first_deriv_partials( + dts, inputs["q"], n_segments=n_segments, n_simpson_intervals_per_segment=n_int_per_seg, order=order + ) - J['dqdt','q'] = wrt_q[2] + J["dqdt", "q"] = wrt_q[2] if segment_names is None: - J['dqdt','dt'] = wrt_dt[2][0] + J["dqdt", "dt"] = wrt_dt[2][0] else: for i_seg, segment_name in enumerate(segment_names): - J['dqdt',segment_name+'|dt'] = wrt_dt[2][i_seg] + J["dqdt", segment_name + "|dt"] = wrt_dt[2][i_seg] diff --git a/openconcept/utilities/math/integrals.py b/openconcept/utilities/math/integrals.py index 05a1ee37..4db8253b 100644 --- a/openconcept/utilities/math/integrals.py +++ b/openconcept/utilities/math/integrals.py @@ -3,7 +3,8 @@ from openmdao.api import ExplicitComponent import warnings -def bdf3_cache_matrix(n,all_bdf=False): + +def bdf3_cache_matrix(n, all_bdf=False): """ This implements the base block Jacobian of the BDF3 method. BDF3 is third order accurate and suitable for stiff systems. @@ -70,56 +71,59 @@ def bdf3_cache_matrix(n,all_bdf=False): # the all_bdf stencil bootstrps the first two points with BDF1 (backward euler) and BDF2 respectively. if all_bdf: - a_diag_1 = np.zeros((n-1,)) - #a_diag_1[0] = 1/2 - a_diag_2 = np.ones((n-1,)) - #a_diag_2[0] = 0 + a_diag_1 = np.zeros((n - 1,)) + # a_diag_1[0] = 1/2 + a_diag_2 = np.ones((n - 1,)) + # a_diag_2[0] = 0 a_diag_2[0] = 1 - a_diag_3 = np.ones((n-1,)) * -18/11 - a_diag_3[0] = -4/3 - a_diag_4 = np.ones((n-1,)) * 9/11 - a_diag_5 = np.ones((n-1,)) * -2/11 - A = sp.diags([a_diag_1, a_diag_2, a_diag_3, a_diag_4, a_diag_5], - [1,0,-1,-2,-3], shape=(n-1,n-1)).asformat('csc') - b_diag = np.ones((n-1,))*6/11 + a_diag_3 = np.ones((n - 1,)) * -18 / 11 + a_diag_3[0] = -4 / 3 + a_diag_4 = np.ones((n - 1,)) * 9 / 11 + a_diag_5 = np.ones((n - 1,)) * -2 / 11 + A = sp.diags( + [a_diag_1, a_diag_2, a_diag_3, a_diag_4, a_diag_5], [1, 0, -1, -2, -3], shape=(n - 1, n - 1) + ).asformat("csc") + b_diag = np.ones((n - 1,)) * 6 / 11 b_diag[0] = 1 - b_diag[1] = 2/3 + b_diag[1] = 2 / 3 else: # otherwise use a full third order stencil as described in the ASCII art above - a_diag_0 = np.zeros((n-1,)) - a_diag_0[0] = -1/6 - a_diag_1 = np.zeros((n-1,)) + a_diag_0 = np.zeros((n - 1,)) + a_diag_0[0] = -1 / 6 + a_diag_1 = np.zeros((n - 1,)) a_diag_1[0] = 1 - a_diag_1[1] = 1/3 - a_diag_2 = np.ones((n-1,)) - a_diag_2[0] = -1/2 - a_diag_2[1] = 1/2 - a_diag_3 = np.ones((n-1,)) * -18/11 + a_diag_1[1] = 1 / 3 + a_diag_2 = np.ones((n - 1,)) + a_diag_2[0] = -1 / 2 + a_diag_2[1] = 1 / 2 + a_diag_3 = np.ones((n - 1,)) * -18 / 11 a_diag_3[0] = -1 - a_diag_4 = np.ones((n-1,)) * 9/11 - a_diag_5 = np.ones((n-1,)) * -2/11 - A = sp.diags([a_diag_0, a_diag_1, a_diag_2, a_diag_3, a_diag_4, a_diag_5], - [2, 1,0,-1,-2,-3], shape=(n-1,n-1)).asformat('csc') + a_diag_4 = np.ones((n - 1,)) * 9 / 11 + a_diag_5 = np.ones((n - 1,)) * -2 / 11 + A = sp.diags( + [a_diag_0, a_diag_1, a_diag_2, a_diag_3, a_diag_4, a_diag_5], [2, 1, 0, -1, -2, -3], shape=(n - 1, n - 1) + ).asformat("csc") - b_diag = np.ones((n-1,))*6/11 + b_diag = np.ones((n - 1,)) * 6 / 11 b_diag[0] = 1 b_diag[1] = 1 - B = sp.diags([b_diag],[0]) + B = sp.diags([b_diag], [0]) # C is the base Jacobian matrix C = sp.linalg.inv(A).dot(B) # we need to offset the entire thing by one row (because the first quantity Q1 is given as an initial condition) # and one column (because we do not make use of the initial derivative dQdt1, as this is a stiff method) # this is the same as saying that Bv = 0 - C = C.asformat('csr') + C = C.asformat("csr") indices = C.nonzero() # the main lower triangular-ish matrix: - tri_mat = sp.csc_matrix((C.data, (indices[0]+1, indices[1]+1))) + tri_mat = sp.csc_matrix((C.data, (indices[0] + 1, indices[1] + 1))) # we need to create a dense matrix of the last row repeated n times for multi-subinterval problems last_row = tri_mat.getrow(-1).toarray() # but we need it in sparse format for openMDAO - repeat_mat = sp.csc_matrix(np.tile(last_row, n).reshape(n,n)) + repeat_mat = sp.csc_matrix(np.tile(last_row, n).reshape(n, n)) return tri_mat, repeat_mat + def simpson_cache_matrix(n): # Simpsons rule defines the "deltas" between each segment as [B] dqdt as follows @@ -132,29 +136,30 @@ def simpson_cache_matrix(n): # 5 8 -1 # -1 8 5 and so on # the row indices are basically 0 0 0 1 1 1 2 2 2 .... - jacmat_rowidx = np.repeat(np.arange((n-1)), 3) + jacmat_rowidx = np.repeat(np.arange((n - 1)), 3) # the column indices are 0 1 2 0 1 2 2 3 4 2 3 4 4 5 6 and so on # so superimpose a 0 1 2 repeating pattern on a 0 0 0 0 0 0 2 2 2 2 2 2 2 repeating pattern - jacmat_colidx = np.repeat(np.arange(0, (n-1), 2), 6) + np.tile(np.arange(3), (n-1)) - jacmat_data = np.tile(np.array([5, 8, -1, -1, 8, 5]) / 12, (n-1) // 2) + jacmat_colidx = np.repeat(np.arange(0, (n - 1), 2), 6) + np.tile(np.arange(3), (n - 1)) + jacmat_data = np.tile(np.array([5, 8, -1, -1, 8, 5]) / 12, (n - 1) // 2) jacmat_base = sp.csr_matrix((jacmat_data, (jacmat_rowidx, jacmat_colidx))) - b = jacmat_base[:,1:] - bv = jacmat_base[:,0] + b = jacmat_base[:, 1:] + bv = jacmat_base[:, 0] - a = sp.diags([-1, 1],[-1, 0],shape=(n-1,n-1)).asformat('csc') + a = sp.diags([-1, 1], [-1, 0], shape=(n - 1, n - 1)).asformat("csc") ia = sp.linalg.inv(a) c = ia.dot(b) cv = ia.dot(bv) - first_row_zeros = sp.csr_matrix(np.zeros((1,n-1))) - tri_mat = sp.bmat([[None, first_row_zeros],[cv, c]]) + first_row_zeros = sp.csr_matrix(np.zeros((1, n - 1))) + tri_mat = sp.bmat([[None, first_row_zeros], [cv, c]]) # we need to create a dense matrix of the last row repeated n times for multi-subinterval problems last_row = tri_mat.getrow(-1).toarray() # but we need it in sparse format for openMDAO - repeat_mat = sp.csc_matrix(np.tile(last_row, n).reshape(n,n)) + repeat_mat = sp.csc_matrix(np.tile(last_row, n).reshape(n, n)) return tri_mat, repeat_mat + def multistep_integrator(q0, dqdt, dts, tri_mat, repeat_mat, segment_names=None, segments_to_count=None, partials=True): """ This implements the base block Jacobian of the BDF3 method. @@ -177,14 +182,14 @@ def multistep_integrator(q0, dqdt, dts, tri_mat, repeat_mat, segment_names=None, count_col = False if i > j and count_col: # repeat mat - col_list.append(repeat_mat*dt) + col_list.append(repeat_mat * dt) elif i == j and count_col: # diagonal - col_list.append(tri_mat*dt) + col_list.append(tri_mat * dt) else: - col_list.append(sp.csr_matrix(([],([],[])),shape=(n,n))) + col_list.append(sp.csr_matrix(([], ([], [])), shape=(n, n))) row_list.append(col_list) - dQdqdt = sp.bmat(row_list).asformat('csr') + dQdqdt = sp.bmat(row_list).asformat("csr") if not partials: Q = dQdqdt.dot(dqdt) + q0 return Q @@ -197,7 +202,7 @@ def multistep_integrator(q0, dqdt, dts, tri_mat, repeat_mat, segment_names=None, if segment_names[j] not in segments_to_count: # skip col IFF not counting this segment count_col = False - #jth segment + # jth segment row_list = [] for i in range(n_segments): # ith row @@ -206,12 +211,13 @@ def multistep_integrator(q0, dqdt, dts, tri_mat, repeat_mat, segment_names=None, elif i == j and count_col: row_list.append([tri_mat]) else: - row_list.append([sp.csr_matrix(([],([],[])),shape=(n,n))]) - dQddt = sp.bmat(row_list).dot(dqdt[j*n:(j+1)*n]) + row_list.append([sp.csr_matrix(([], ([], [])), shape=(n, n))]) + dQddt = sp.bmat(row_list).dot(dqdt[j * n : (j + 1) * n]) dt_partials_list.append(sp.csr_matrix(dQddt).transpose()) return dQdqdt, dt_partials_list + # def three_point_lagrange_integration(dqdt, dts, num_segments=1, num_intervals=2,): # """This method integrates a rate over time using a 3 point Lagrange interpolant # Similar to Simpson's rule except extended to provide increments at every subinterval @@ -439,38 +445,39 @@ def multistep_integrator(q0, dqdt, dts, tri_mat, repeat_mat, segment_names=None, # return delta_q, partials_wrt_dqdt, partials_wrt_dts # def integrator_partials_wrt_deltas(num_segments, num_intervals): - # """ - # This function computes partials of an integrated quantity with respect to the "delta quantity per interval" - # in the context of openConcept's Simpson's rule approximated integration technique. - - # Inputs - # ------ - # num_segments : float - # Number of mission segments to integrate (scalar) - # num_intervals : float - # Number of Simpson intervals per segment (scalar) - - # Outputs - # ------- - # partial_q_wrt_deltas : float - # A sparse (CSR) matrix representation of the partial derivatives of q - # with respect to the delta quantity per half-interval - # Dimension is nn * num_segments (rows) by (nn -1) * num_segments (cols) - # where nn = (2 * num_intervals + 1) - - # """ - # nn = num_intervals * 2 + 1 - # # the basic structure of the jacobian is lower triangular (all late values depend on all early ones) - # jacmat = np.tril(np.ones((num_segments*(nn-1),num_segments*(nn-1)))) - # # the first entry of q has no dependence on the deltas so insert a row of zeros - # jacmat = np.insert(jacmat,0,np.zeros(num_segments*(nn-1)),axis=0) - # for i in range(1,num_segments): - # # since the end of each segment is equal to the beginning of the next - # # duplicate the jacobian row once at the end of each segment - # duplicate_row = jacmat[nn*i-1,:] - # jacmat = np.insert(jacmat,nn*i,duplicate_row,axis=0) - # partials_q_wrt_deltas = sp.csr_matrix(jacmat) - # return partials_q_wrt_deltas +# """ +# This function computes partials of an integrated quantity with respect to the "delta quantity per interval" +# in the context of openConcept's Simpson's rule approximated integration technique. + +# Inputs +# ------ +# num_segments : float +# Number of mission segments to integrate (scalar) +# num_intervals : float +# Number of Simpson intervals per segment (scalar) + +# Outputs +# ------- +# partial_q_wrt_deltas : float +# A sparse (CSR) matrix representation of the partial derivatives of q +# with respect to the delta quantity per half-interval +# Dimension is nn * num_segments (rows) by (nn -1) * num_segments (cols) +# where nn = (2 * num_intervals + 1) + +# """ +# nn = num_intervals * 2 + 1 +# # the basic structure of the jacobian is lower triangular (all late values depend on all early ones) +# jacmat = np.tril(np.ones((num_segments*(nn-1),num_segments*(nn-1)))) +# # the first entry of q has no dependence on the deltas so insert a row of zeros +# jacmat = np.insert(jacmat,0,np.zeros(num_segments*(nn-1)),axis=0) +# for i in range(1,num_segments): +# # since the end of each segment is equal to the beginning of the next +# # duplicate the jacobian row once at the end of each segment +# duplicate_row = jacmat[nn*i-1,:] +# jacmat = np.insert(jacmat,nn*i,duplicate_row,axis=0) +# partials_q_wrt_deltas = sp.csr_matrix(jacmat) +# return partials_q_wrt_deltas + class Integrator(ExplicitComponent): """ @@ -486,7 +493,7 @@ class Integrator(ExplicitComponent): Rate to integrate (vector) q_initial : float Starting value of quantity (scalar) - + Outputs ------- q : float @@ -511,162 +518,199 @@ class Integrator(ExplicitComponent): 'duration' creates input 'duration' 'bounds' creates inputs 't_initial', 't_final' """ + def __init__(self, **kwargs): super(Integrator, self).__init__(**kwargs) self._state_vars = {} - num_nodes = self.options['num_nodes'] - method = self.options['method'] + num_nodes = self.options["num_nodes"] + method = self.options["method"] # check to make sure num nodes is OK if (num_nodes - 1) % 2 > 0: - raise ValueError('num_nodes is ' +str(num_nodes) + ' and must be odd') - + raise ValueError("num_nodes is " + str(num_nodes) + " and must be odd") + if num_nodes > 1: - if method == 'bdf3': + if method == "bdf3": self.tri_mat, self.repeat_mat = bdf3_cache_matrix(num_nodes) - elif method == 'simpson': + elif method == "simpson": self.tri_mat, self.repeat_mat = simpson_cache_matrix(num_nodes) def initialize(self): - self.options.declare('diff_units',default=None, desc="Units of the differential") - self.options.declare('num_nodes',default=11, desc="Analysis points per segment") - self.options.declare('method',default='bdf3', desc="Numerical method to use.") - self.options.declare('time_setup',default='dt') - - def add_integrand(self, name, rate_name=None, start_name=None, end_name=None, val=0.0, start_val=0.0, - units=None, rate_units=None, zero_start=False, final_only=False, lower=-1e30, upper=1e30): - """ - Add a new integrated variable q = integrate(dqdt) + q0 - This will add an output with the integrated quantity, an output with the final value, - an input with the rate source, and an input for the initial quantity. - - Parameters - ---------- - name : str - The name of the integrated variable to be created. - rate_name : str - The name of the input rate (default name"_rate") - start_name : str - The name of the initial value input (default value name"_initial") - end_name : str - The name of the end value output (default value name"_final") - units : str or None - Units for the integrated quantity (or inferred automatically from rate_units) - rate_units : str or None - Units of the rate (can be inferred automatically from units) - zero_start : bool - If true, eliminates start value input and always begins from zero (default False) - final_only : bool - If true, only integrates final quantity, not all the intermediate points (default False) - val : float - Default value for the integrated output (default 0.0) - Can be scalar or shape num_nodes - start_val : float - Default value for the initial value input (default 0.0) - upper : float - Upper bound on integrated quantity - lower : float - Lower bound on integrated quantity - """ - - num_nodes = self.options['num_nodes'] - diff_units = self.options['diff_units'] - time_setup = self.options['time_setup'] - - if units and rate_units: - raise ValueError('Specify either quantity units or rate units, but not both') - if units: - # infer rate units from diff units and quantity units - if not diff_units: - rate_units = units - warnings.warn('You have specified a integral with respect to a unitless integrand. Be aware of this.') - else: - rate_units = '('+units+') / (' + diff_units +')' - elif rate_units: - # infer quantity units from rate units and diff units - if not diff_units: - units = rate_units - warnings.warn('You have specified a integral with respect to a unitless integrand. Be aware of this.') - else: - units = '('+rate_units+') * (' + diff_units + ')' - elif diff_units: - # neither quantity nor rate units specified - rate_units = '(' + diff_units +')** -1' - - if not rate_name: - rate_name = name + '_rate' - if not start_name: - start_name = name + '_initial' - if not end_name: - end_name = name + '_final' - - options = {'name': name, - 'rate_name': rate_name, - 'start_name': start_name, - 'start_val': start_val, - 'end_name': end_name, - 'units': units, - 'rate_units': rate_units, - 'zero_start': zero_start, - 'final_only': final_only, - 'upper': upper, - 'lower': lower} - - # TODO maybe later can pass kwargs - self._state_vars[name] = options - if not hasattr(val, '__len__'): - # scalar - default_final_val = val + self.options.declare("diff_units", default=None, desc="Units of the differential") + self.options.declare("num_nodes", default=11, desc="Analysis points per segment") + self.options.declare("method", default="bdf3", desc="Numerical method to use.") + self.options.declare("time_setup", default="dt") + + def add_integrand( + self, + name, + rate_name=None, + start_name=None, + end_name=None, + val=0.0, + start_val=0.0, + units=None, + rate_units=None, + zero_start=False, + final_only=False, + lower=-1e30, + upper=1e30, + ): + """ + Add a new integrated variable q = integrate(dqdt) + q0 + This will add an output with the integrated quantity, an output with the final value, + an input with the rate source, and an input for the initial quantity. + + Parameters + ---------- + name : str + The name of the integrated variable to be created. + rate_name : str + The name of the input rate (default name"_rate") + start_name : str + The name of the initial value input (default value name"_initial") + end_name : str + The name of the end value output (default value name"_final") + units : str or None + Units for the integrated quantity (or inferred automatically from rate_units) + rate_units : str or None + Units of the rate (can be inferred automatically from units) + zero_start : bool + If true, eliminates start value input and always begins from zero (default False) + final_only : bool + If true, only integrates final quantity, not all the intermediate points (default False) + val : float + Default value for the integrated output (default 0.0) + Can be scalar or shape num_nodes + start_val : float + Default value for the initial value input (default 0.0) + upper : float + Upper bound on integrated quantity + lower : float + Lower bound on integrated quantity + """ + + num_nodes = self.options["num_nodes"] + diff_units = self.options["diff_units"] + time_setup = self.options["time_setup"] + + if units and rate_units: + raise ValueError("Specify either quantity units or rate units, but not both") + if units: + # infer rate units from diff units and quantity units + if not diff_units: + rate_units = units + warnings.warn("You have specified a integral with respect to a unitless integrand. Be aware of this.") else: - # vector - default_final_val = val[-1] + rate_units = "(" + units + ") / (" + diff_units + ")" + elif rate_units: + # infer quantity units from rate units and diff units + if not diff_units: + units = rate_units + warnings.warn("You have specified a integral with respect to a unitless integrand. Be aware of this.") + else: + units = "(" + rate_units + ") * (" + diff_units + ")" + elif diff_units: + # neither quantity nor rate units specified + rate_units = "(" + diff_units + ")** -1" + + if not rate_name: + rate_name = name + "_rate" + if not start_name: + start_name = name + "_initial" + if not end_name: + end_name = name + "_final" + + options = { + "name": name, + "rate_name": rate_name, + "start_name": start_name, + "start_val": start_val, + "end_name": end_name, + "units": units, + "rate_units": rate_units, + "zero_start": zero_start, + "final_only": final_only, + "upper": upper, + "lower": lower, + } + + # TODO maybe later can pass kwargs + self._state_vars[name] = options + if not hasattr(val, "__len__"): + # scalar + default_final_val = val + else: + # vector + default_final_val = val[-1] - self.add_input(rate_name, val=0.0, shape=(num_nodes), units=rate_units) - self.add_output(end_name, units=units, val=default_final_val, upper=options['upper'],lower=options['lower']) + self.add_input(rate_name, val=0.0, shape=(num_nodes), units=rate_units) + self.add_output(end_name, units=units, val=default_final_val, upper=options["upper"], lower=options["lower"]) + if not final_only: + self.add_output( + name, shape=(num_nodes), val=val, units=units, upper=options["upper"], lower=options["lower"] + ) + if not zero_start: + self.add_input(start_name, val=start_val, units=units) if not final_only: - self.add_output(name, shape=(num_nodes), val=val, units=units, upper=options['upper'],lower=options['lower']) - if not zero_start: - self.add_input(start_name, val=start_val, units=units) - if not final_only: - self.declare_partials([name], [start_name], rows=np.arange(num_nodes), cols=np.zeros((num_nodes,)), val=np.ones((num_nodes,))) - self.declare_partials([end_name], [start_name], val=1) - - # set up sparse partial structure - if num_nodes > 1: - # single point analysis has no dqdt dependency since the outputs are equal to the inputs - dQdrate, dQddtlist = multistep_integrator(0, np.ones((num_nodes,)), np.ones((1,)), self.tri_mat, self.repeat_mat, - segment_names=None, segments_to_count=None, partials=True) - dQdrate_indices = dQdrate.nonzero() - dQfdrate_indices = dQdrate.getrow(-1).nonzero() - if not final_only: - self.declare_partials([name], [rate_name], rows=dQdrate_indices[0], cols=dQdrate_indices[1]) - self.declare_partials([end_name], [rate_name], rows=dQfdrate_indices[0], cols=dQfdrate_indices[1]) # rows are zeros - - dQddt_seg = dQddtlist[0] - dQddt_indices = dQddt_seg.nonzero() - dQfddt_indices = dQddt_seg.getrow(-1).nonzero() + self.declare_partials( + [name], + [start_name], + rows=np.arange(num_nodes), + cols=np.zeros((num_nodes,)), + val=np.ones((num_nodes,)), + ) + self.declare_partials([end_name], [start_name], val=1) + + # set up sparse partial structure + if num_nodes > 1: + # single point analysis has no dqdt dependency since the outputs are equal to the inputs + dQdrate, dQddtlist = multistep_integrator( + 0, + np.ones((num_nodes,)), + np.ones((1,)), + self.tri_mat, + self.repeat_mat, + segment_names=None, + segments_to_count=None, + partials=True, + ) + dQdrate_indices = dQdrate.nonzero() + dQfdrate_indices = dQdrate.getrow(-1).nonzero() + if not final_only: + self.declare_partials([name], [rate_name], rows=dQdrate_indices[0], cols=dQdrate_indices[1]) + self.declare_partials( + [end_name], [rate_name], rows=dQfdrate_indices[0], cols=dQfdrate_indices[1] + ) # rows are zeros - if time_setup == 'dt': - if not final_only: - self.declare_partials([name], ['dt'], rows=dQddt_indices[0], cols=dQddt_indices[1]) - self.declare_partials([end_name], ['dt'], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) - elif time_setup == 'duration': - if not final_only: - self.declare_partials([name], ['duration'], rows=dQddt_indices[0], cols=dQddt_indices[1]) - self.declare_partials([end_name], ['duration'], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) - elif time_setup == 'bounds': - if not final_only: - self.declare_partials([name], ['t_initial','t_final'], rows=dQddt_indices[0], cols=dQddt_indices[1]) - self.declare_partials([end_name], ['t_initial','t_final'], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) - else: - raise ValueError('Only dt, duration, and bounds are allowable values of time_setup') + dQddt_seg = dQddtlist[0] + dQddt_indices = dQddt_seg.nonzero() + dQfddt_indices = dQddt_seg.getrow(-1).nonzero() + if time_setup == "dt": + if not final_only: + self.declare_partials([name], ["dt"], rows=dQddt_indices[0], cols=dQddt_indices[1]) + self.declare_partials([end_name], ["dt"], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) + elif time_setup == "duration": + if not final_only: + self.declare_partials([name], ["duration"], rows=dQddt_indices[0], cols=dQddt_indices[1]) + self.declare_partials([end_name], ["duration"], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) + elif time_setup == "bounds": + if not final_only: + self.declare_partials( + [name], ["t_initial", "t_final"], rows=dQddt_indices[0], cols=dQddt_indices[1] + ) + self.declare_partials( + [end_name], ["t_initial", "t_final"], rows=dQfddt_indices[0], cols=dQfddt_indices[1] + ) + else: + raise ValueError("Only dt, duration, and bounds are allowable values of time_setup") def setup(self): - diff_units = self.options['diff_units'] - num_nodes = self.options['num_nodes'] - method = self.options['method'] - time_setup = self.options['time_setup'] + diff_units = self.options["diff_units"] + num_nodes = self.options["num_nodes"] + method = self.options["method"] + time_setup = self.options["time_setup"] # branch logic here for the corner case of 0 segments # so point analysis can be run without breaking everything @@ -675,117 +719,131 @@ def setup(self): else: single_point = False if not single_point: - if method == 'bdf3': + if method == "bdf3": self.tri_mat, self.repeat_mat = bdf3_cache_matrix(num_nodes) - elif method == 'simpson': + elif method == "simpson": self.tri_mat, self.repeat_mat = simpson_cache_matrix(num_nodes) - if time_setup == 'dt': - self.add_input('dt', units=diff_units, desc='Time step') - elif time_setup == 'duration': - self.add_input('duration', units=diff_units, desc='Time duration') - elif time_setup == 'bounds': - self.add_input('t_initial', units=diff_units, desc='Initial time') - self.add_input('t_final', units=diff_units, desc='Initial time') + if time_setup == "dt": + self.add_input("dt", units=diff_units, desc="Time step") + elif time_setup == "duration": + self.add_input("duration", units=diff_units, desc="Time duration") + elif time_setup == "bounds": + self.add_input("t_initial", units=diff_units, desc="Initial time") + self.add_input("t_final", units=diff_units, desc="Initial time") else: - raise ValueError('Only dt, duration, and bounds are allowable values of time_setup') - + raise ValueError("Only dt, duration, and bounds are allowable values of time_setup") def compute(self, inputs, outputs): - num_nodes = self.options['num_nodes'] - time_setup=self.options['time_setup'] + num_nodes = self.options["num_nodes"] + time_setup = self.options["time_setup"] if num_nodes == 1: single_point = True else: single_point = False - if time_setup == 'dt': - dts = [inputs['dt'][0]] - elif time_setup == 'duration': + if time_setup == "dt": + dts = [inputs["dt"][0]] + elif time_setup == "duration": if num_nodes == 1: - dts = [inputs['duration'][0]] + dts = [inputs["duration"][0]] else: - dts = [inputs['duration'][0]/(num_nodes-1)] - elif time_setup == 'bounds': - delta_t = inputs['t_final'] - inputs['t_initial'] - dts = [delta_t[0]/(num_nodes-1)] - + dts = [inputs["duration"][0] / (num_nodes - 1)] + elif time_setup == "bounds": + delta_t = inputs["t_final"] - inputs["t_initial"] + dts = [delta_t[0] / (num_nodes - 1)] + for name, options in self._state_vars.items(): - if options['zero_start']: + if options["zero_start"]: q0 = np.array([0.0]) else: - q0 = inputs[options['start_name']] + q0 = inputs[options["start_name"]] if not single_point: - Q = multistep_integrator(q0, inputs[options['rate_name']], dts, self.tri_mat, self.repeat_mat, - segment_names=None, segments_to_count=None, partials=False) + Q = multistep_integrator( + q0, + inputs[options["rate_name"]], + dts, + self.tri_mat, + self.repeat_mat, + segment_names=None, + segments_to_count=None, + partials=False, + ) else: # single point case, no change, no dependence on time Q = q0 - if not options['final_only']: - outputs[options['name']] = Q - outputs[options['end_name']] = Q[-1] + if not options["final_only"]: + outputs[options["name"]] = Q + outputs[options["end_name"]] = Q[-1] def compute_partials(self, inputs, J): - num_nodes = self.options['num_nodes'] - time_setup = self.options['time_setup'] + num_nodes = self.options["num_nodes"] + time_setup = self.options["time_setup"] if num_nodes == 1: single_point = True else: single_point = False if not single_point: - if time_setup == 'dt': - dts = [inputs['dt'][0]] - elif time_setup == 'duration': - dts = [inputs['duration'][0]/(num_nodes-1)] - elif time_setup == 'bounds': - delta_t = inputs['t_final'] - inputs['t_initial'] - dts = [delta_t[0]/(num_nodes-1)] - + if time_setup == "dt": + dts = [inputs["dt"][0]] + elif time_setup == "duration": + dts = [inputs["duration"][0] / (num_nodes - 1)] + elif time_setup == "bounds": + delta_t = inputs["t_final"] - inputs["t_initial"] + dts = [delta_t[0] / (num_nodes - 1)] + for name, options in self._state_vars.items(): - start_name = options['start_name'] - end_name = options['end_name'] - qty_name = options['name'] - rate_name = options['rate_name'] - final_only = options['final_only'] - if options['zero_start']: + start_name = options["start_name"] + end_name = options["end_name"] + qty_name = options["name"] + rate_name = options["rate_name"] + final_only = options["final_only"] + if options["zero_start"]: q0 = 0 else: q0 = inputs[start_name] - dQdrate, dQddtlist = multistep_integrator(q0, inputs[rate_name], dts, self.tri_mat, self.repeat_mat, - segment_names=None, segments_to_count=None, partials=True) + dQdrate, dQddtlist = multistep_integrator( + q0, + inputs[rate_name], + dts, + self.tri_mat, + self.repeat_mat, + segment_names=None, + segments_to_count=None, + partials=True, + ) if not final_only: J[qty_name, rate_name] = dQdrate.data J[end_name, rate_name] = dQdrate.getrow(-1).data - if time_setup == 'dt': + if time_setup == "dt": if not final_only: - J[qty_name, 'dt'] = np.squeeze(dQddtlist[0].toarray()[1:]) - J[end_name, 'dt'] = np.squeeze(dQddtlist[0].getrow(-1).toarray()) + J[qty_name, "dt"] = np.squeeze(dQddtlist[0].toarray()[1:]) + J[end_name, "dt"] = np.squeeze(dQddtlist[0].getrow(-1).toarray()) - elif time_setup == 'duration': + elif time_setup == "duration": if not final_only: - J[qty_name, 'duration'] = np.squeeze(dQddtlist[0].toarray()[1:] / (num_nodes - 1)) - J[end_name, 'duration'] = np.squeeze(dQddtlist[0].getrow(-1).toarray() / (num_nodes - 1)) + J[qty_name, "duration"] = np.squeeze(dQddtlist[0].toarray()[1:] / (num_nodes - 1)) + J[end_name, "duration"] = np.squeeze(dQddtlist[0].getrow(-1).toarray() / (num_nodes - 1)) - elif time_setup == 'bounds': + elif time_setup == "bounds": if not final_only: if len(dQddtlist[0].data) == 0: - J[qty_name, 't_initial'] = np.zeros(J[qty_name, 't_initial'].shape) - J[qty_name, 't_final'] = np.zeros(J[qty_name, 't_final'].shape) + J[qty_name, "t_initial"] = np.zeros(J[qty_name, "t_initial"].shape) + J[qty_name, "t_final"] = np.zeros(J[qty_name, "t_final"].shape) else: - J[qty_name, 't_initial'] = -dQddtlist[0].data / (num_nodes - 1) - J[qty_name, 't_final'] = dQddtlist[0].data / (num_nodes - 1) + J[qty_name, "t_initial"] = -dQddtlist[0].data / (num_nodes - 1) + J[qty_name, "t_final"] = dQddtlist[0].data / (num_nodes - 1) if len(dQddtlist[0].getrow(-1).data) == 0: - J[end_name, 't_initial'] = 0 - J[end_name, 't_final'] = 0 + J[end_name, "t_initial"] = 0 + J[end_name, "t_final"] = 0 else: - J[end_name, 't_initial'] = -dQddtlist[0].getrow(-1).data / (num_nodes - 1) - J[end_name, 't_final'] = dQddtlist[0].getrow(-1).data / (num_nodes - 1) - + J[end_name, "t_initial"] = -dQddtlist[0].getrow(-1).data / (num_nodes - 1) + J[end_name, "t_final"] = dQddtlist[0].getrow(-1).data / (num_nodes - 1) class OldIntegrator(ExplicitComponent): @@ -844,33 +902,33 @@ class OldIntegrator(ExplicitComponent): """ def initialize(self): - self.options.declare('segment_names', default=None, desc="Names of differentiation segments") - self.options.declare('segments_to_count', default=None, desc="Names of differentiation segments") - self.options.declare('quantity_units',default=None, desc="Units of the quantity being differentiated") - self.options.declare('diff_units',default=None, desc="Units of the differential") - self.options.declare('rate_units',default=None, desc="Units of the rate being integrated") - self.options.declare('num_nodes',default=11, desc="Analysis points per segment") - self.options.declare('method',default='bdf3', desc="Numerical method to use.") - self.options.declare('zero_start',default=False) - self.options.declare('final_only',default=False) - self.options.declare('lower',default=-1e30) - self.options.declare('upper',default=1e30) - self.options.declare('time_setup',default='dt') + self.options.declare("segment_names", default=None, desc="Names of differentiation segments") + self.options.declare("segments_to_count", default=None, desc="Names of differentiation segments") + self.options.declare("quantity_units", default=None, desc="Units of the quantity being differentiated") + self.options.declare("diff_units", default=None, desc="Units of the differential") + self.options.declare("rate_units", default=None, desc="Units of the rate being integrated") + self.options.declare("num_nodes", default=11, desc="Analysis points per segment") + self.options.declare("method", default="bdf3", desc="Numerical method to use.") + self.options.declare("zero_start", default=False) + self.options.declare("final_only", default=False) + self.options.declare("lower", default=-1e30) + self.options.declare("upper", default=1e30) + self.options.declare("time_setup", default="dt") def setup(self): - segment_names = self.options['segment_names'] - segments_to_count = self.options['segments_to_count'] - quantity_units = self.options['quantity_units'] - diff_units = self.options['diff_units'] - num_nodes = self.options['num_nodes'] - method = self.options['method'] - zero_start = self.options['zero_start'] - final_only = self.options['final_only'] - time_setup = self.options['time_setup'] + segment_names = self.options["segment_names"] + segments_to_count = self.options["segments_to_count"] + quantity_units = self.options["quantity_units"] + diff_units = self.options["diff_units"] + num_nodes = self.options["num_nodes"] + method = self.options["method"] + zero_start = self.options["zero_start"] + final_only = self.options["final_only"] + time_setup = self.options["time_setup"] # check to make sure num nodes is OK if (num_nodes - 1) % 2 > 0: - raise ValueError('num_nodes must be odd') + raise ValueError("num_nodes must be odd") # branch logic here for the corner case of 0 segments # so point analysis can be run without breaking everything @@ -879,9 +937,9 @@ def setup(self): else: single_point = False if not single_point: - if method == 'bdf3': + if method == "bdf3": self.tri_mat, self.repeat_mat = bdf3_cache_matrix(num_nodes) - elif method == 'simpson': + elif method == "simpson": self.tri_mat, self.repeat_mat = simpson_cache_matrix(num_nodes) if segment_names is None: @@ -895,90 +953,123 @@ def setup(self): if quantity_units is None and diff_units is None: rate_units = None elif quantity_units is None: - rate_units = '(' + diff_units +')** -1' + rate_units = "(" + diff_units + ")** -1" elif diff_units is None: rate_units = quantity_units - warnings.warn('You have specified a integral with respect to a unitless integrand. Be aware of this.') + warnings.warn("You have specified a integral with respect to a unitless integrand. Be aware of this.") else: - rate_units = '('+quantity_units+') / (' + diff_units +')' + rate_units = "(" + quantity_units + ") / (" + diff_units + ")" # the output of this function is of length nn - 1. NO partial for first row (initial value) # get the partials of the delta quantities WRT the rates dDelta / drate - self.add_input('dqdt', val=0, units=rate_units, desc='Quantity to integrate',shape=(nn_tot,)) - self.add_output('q_final', units=quantity_units, desc='Final value of q',upper=self.options['upper'],lower=self.options['lower']) + self.add_input("dqdt", val=0, units=rate_units, desc="Quantity to integrate", shape=(nn_tot,)) + self.add_output( + "q_final", + units=quantity_units, + desc="Final value of q", + upper=self.options["upper"], + lower=self.options["lower"], + ) if not final_only: - self.add_output('q', units=quantity_units, desc='Integral of dqdt', shape=(nn_tot,),upper=self.options['upper'],lower=self.options['lower']) + self.add_output( + "q", + units=quantity_units, + desc="Integral of dqdt", + shape=(nn_tot,), + upper=self.options["upper"], + lower=self.options["lower"], + ) if not zero_start: - self.add_input('q_initial', val=0, units=quantity_units, desc='Initial value') + self.add_input("q_initial", val=0, units=quantity_units, desc="Initial value") if not final_only: - self.declare_partials(['q'], ['q_initial'], rows=np.arange(nn_tot), cols=np.zeros((nn_tot,)), val=np.ones((nn_tot,))) - self.declare_partials(['q_final'], ['q_initial'], val=1) + self.declare_partials( + ["q"], ["q_initial"], rows=np.arange(nn_tot), cols=np.zeros((nn_tot,)), val=np.ones((nn_tot,)) + ) + self.declare_partials(["q_final"], ["q_initial"], val=1) if not single_point: # single point analysis has no dqdt dependency since the outputs are equal to the inputs - dQdrate, dQddtlist = multistep_integrator(0, np.ones((nn_tot,)), np.ones((n_segments,)), self.tri_mat, self.repeat_mat, - segment_names=segment_names, segments_to_count=segments_to_count, partials=True) + dQdrate, dQddtlist = multistep_integrator( + 0, + np.ones((nn_tot,)), + np.ones((n_segments,)), + self.tri_mat, + self.repeat_mat, + segment_names=segment_names, + segments_to_count=segments_to_count, + partials=True, + ) dQdrate_indices = dQdrate.nonzero() dQfdrate_indices = dQdrate.getrow(-1).nonzero() if not final_only: - self.declare_partials(['q'], ['dqdt'], rows=dQdrate_indices[0], cols=dQdrate_indices[1]) - self.declare_partials(['q_final'], ['dqdt'], rows=dQfdrate_indices[0], cols=dQfdrate_indices[1]) # rows are zeros + self.declare_partials(["q"], ["dqdt"], rows=dQdrate_indices[0], cols=dQdrate_indices[1]) + self.declare_partials( + ["q_final"], ["dqdt"], rows=dQfdrate_indices[0], cols=dQfdrate_indices[1] + ) # rows are zeros if segment_names is None: dQddt_seg = dQddtlist[0] dQddt_indices = dQddt_seg.nonzero() dQfddt_indices = dQddt_seg.getrow(-1).nonzero() - if time_setup == 'dt': - self.add_input('dt', units=diff_units, desc='Time step') + if time_setup == "dt": + self.add_input("dt", units=diff_units, desc="Time step") if not final_only: - self.declare_partials(['q'], ['dt'], rows=dQddt_indices[0], cols=dQddt_indices[1]) - self.declare_partials(['q_final'], ['dt'], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) - elif time_setup == 'duration': - self.add_input('duration', units=diff_units, desc='Time duration') + self.declare_partials(["q"], ["dt"], rows=dQddt_indices[0], cols=dQddt_indices[1]) + self.declare_partials(["q_final"], ["dt"], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) + elif time_setup == "duration": + self.add_input("duration", units=diff_units, desc="Time duration") if not final_only: - self.declare_partials(['q'], ['duration'], rows=dQddt_indices[0], cols=dQddt_indices[1]) - self.declare_partials(['q_final'], ['duration'], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) - elif time_setup == 'bounds': - self.add_input('t_initial', units=diff_units, desc='Initial time') - self.add_input('t_final', units=diff_units, desc='Initial time') + self.declare_partials(["q"], ["duration"], rows=dQddt_indices[0], cols=dQddt_indices[1]) + self.declare_partials(["q_final"], ["duration"], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) + elif time_setup == "bounds": + self.add_input("t_initial", units=diff_units, desc="Initial time") + self.add_input("t_final", units=diff_units, desc="Initial time") if not final_only: - self.declare_partials(['q'], ['t_initial','t_final'], rows=dQddt_indices[0], cols=dQddt_indices[1]) - self.declare_partials(['q_final'], ['t_initial','t_final'], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) + self.declare_partials( + ["q"], ["t_initial", "t_final"], rows=dQddt_indices[0], cols=dQddt_indices[1] + ) + self.declare_partials( + ["q_final"], ["t_initial", "t_final"], rows=dQfddt_indices[0], cols=dQfddt_indices[1] + ) else: - raise ValueError('Only dt, duration, and bounds are allowable values of time_setup') + raise ValueError("Only dt, duration, and bounds are allowable values of time_setup") else: - if time_setup != 'dt': - raise ValueError('dt is the only time_setup supported for multisegment integrations') + if time_setup != "dt": + raise ValueError("dt is the only time_setup supported for multisegment integrations") for i_seg, segment_name in enumerate(segment_names): - self.add_input(segment_name +'|dt', units=diff_units, desc='Time step') + self.add_input(segment_name + "|dt", units=diff_units, desc="Time step") dQddt_seg = dQddtlist[i_seg] dQddt_indices = dQddt_seg.nonzero() dQfddt_indices = dQddt_seg.getrow(-1).nonzero() if not final_only: - self.declare_partials(['q'], [segment_name +'|dt'], rows=dQddt_indices[0], cols=dQddt_indices[1]) - self.declare_partials(['q_final'], [segment_name +'|dt'], rows=dQfddt_indices[0], cols=dQfddt_indices[1]) + self.declare_partials( + ["q"], [segment_name + "|dt"], rows=dQddt_indices[0], cols=dQddt_indices[1] + ) + self.declare_partials( + ["q_final"], [segment_name + "|dt"], rows=dQfddt_indices[0], cols=dQfddt_indices[1] + ) else: - if time_setup == 'dt': - self.add_input('dt', units=diff_units, desc='Time step') - elif time_setup == 'duration': - self.add_input('duration', units=diff_units, desc='Time duration') - elif time_setup == 'bounds': - self.add_input('t_initial', units=diff_units, desc='Initial time') - self.add_input('t_final', units=diff_units, desc='Initial time') + if time_setup == "dt": + self.add_input("dt", units=diff_units, desc="Time step") + elif time_setup == "duration": + self.add_input("duration", units=diff_units, desc="Time duration") + elif time_setup == "bounds": + self.add_input("t_initial", units=diff_units, desc="Initial time") + self.add_input("t_final", units=diff_units, desc="Initial time") else: - raise ValueError('Only dt, duration, and bounds are allowable values of time_setup') + raise ValueError("Only dt, duration, and bounds are allowable values of time_setup") def compute(self, inputs, outputs): - segment_names = self.options['segment_names'] - num_nodes = self.options['num_nodes'] - segments_to_count = self.options['segments_to_count'] - zero_start = self.options['zero_start'] - final_only = self.options['final_only'] - time_setup=self.options['time_setup'] + segment_names = self.options["segment_names"] + num_nodes = self.options["num_nodes"] + segments_to_count = self.options["segments_to_count"] + zero_start = self.options["zero_start"] + final_only = self.options["final_only"] + time_setup = self.options["time_setup"] if num_nodes == 1: single_point = True @@ -987,49 +1078,54 @@ def compute(self, inputs, outputs): if segment_names is None: n_segments = 1 - if time_setup == 'dt': - dts = [inputs['dt'][0]] - elif time_setup == 'duration': + if time_setup == "dt": + dts = [inputs["dt"][0]] + elif time_setup == "duration": if num_nodes == 1: - dts = [inputs['duration'][0]] + dts = [inputs["duration"][0]] else: - dts = [inputs['duration'][0]/(num_nodes-1)] - elif time_setup == 'bounds': - delta_t = inputs['t_final'] - inputs['t_initial'] - dts = [delta_t[0]/(num_nodes-1)] + dts = [inputs["duration"][0] / (num_nodes - 1)] + elif time_setup == "bounds": + delta_t = inputs["t_final"] - inputs["t_initial"] + dts = [delta_t[0] / (num_nodes - 1)] else: n_segments = len(segment_names) dts = [] for i_seg, segment_name in enumerate(segment_names): - input_name = segment_name+'|dt' + input_name = segment_name + "|dt" dts.append(inputs[input_name][0]) if zero_start: q0 = 0 else: - q0 = inputs['q_initial'] + q0 = inputs["q_initial"] if not single_point: - Q = multistep_integrator(q0, inputs['dqdt'], dts, self.tri_mat, self.repeat_mat, - segment_names=segment_names, segments_to_count=segments_to_count, partials=False) + Q = multistep_integrator( + q0, + inputs["dqdt"], + dts, + self.tri_mat, + self.repeat_mat, + segment_names=segment_names, + segments_to_count=segments_to_count, + partials=False, + ) else: # single point case, no change, no dependence on time Q = q0 if not final_only: - outputs['q'] = Q - outputs['q_final'] = Q[-1] - - - + outputs["q"] = Q + outputs["q_final"] = Q[-1] def compute_partials(self, inputs, J): - segment_names = self.options['segment_names'] - quantity_units = self.options['quantity_units'] - diff_units = self.options['diff_units'] - num_nodes = self.options['num_nodes'] - segments_to_count = self.options['segments_to_count'] - zero_start = self.options['zero_start'] - final_only = self.options['final_only'] - time_setup = self.options['time_setup'] + segment_names = self.options["segment_names"] + quantity_units = self.options["quantity_units"] + diff_units = self.options["diff_units"] + num_nodes = self.options["num_nodes"] + segments_to_count = self.options["segments_to_count"] + zero_start = self.options["zero_start"] + final_only = self.options["final_only"] + time_setup = self.options["time_setup"] if num_nodes == 1: single_point = True @@ -1044,74 +1140,82 @@ def compute_partials(self, inputs, J): if segment_names is None: n_segments = 1 - if time_setup == 'dt': - dts = [inputs['dt'][0]] - elif time_setup == 'duration': - dts = [inputs['duration'][0]/(num_nodes-1)] - elif time_setup == 'bounds': - delta_t = inputs['t_final'] - inputs['t_initial'] - dts = [delta_t[0]/(num_nodes-1)] + if time_setup == "dt": + dts = [inputs["dt"][0]] + elif time_setup == "duration": + dts = [inputs["duration"][0] / (num_nodes - 1)] + elif time_setup == "bounds": + delta_t = inputs["t_final"] - inputs["t_initial"] + dts = [delta_t[0] / (num_nodes - 1)] else: n_segments = len(segment_names) dts = [] for i_seg, segment_name in enumerate(segment_names): - input_name = segment_name+'|dt' + input_name = segment_name + "|dt" dts.append(inputs[input_name][0]) if zero_start: q0 = 0 else: - q0 = inputs['q_initial'] - dQdrate, dQddtlist = multistep_integrator(q0, inputs['dqdt'], dts, self.tri_mat, self.repeat_mat, - segment_names=segment_names, segments_to_count=segments_to_count, partials=True) + q0 = inputs["q_initial"] + dQdrate, dQddtlist = multistep_integrator( + q0, + inputs["dqdt"], + dts, + self.tri_mat, + self.repeat_mat, + segment_names=segment_names, + segments_to_count=segments_to_count, + partials=True, + ) if not final_only: - J['q','dqdt'] = dQdrate.data - J['q_final', 'dqdt'] = dQdrate.getrow(-1).data + J["q", "dqdt"] = dQdrate.data + J["q_final", "dqdt"] = dQdrate.getrow(-1).data if segment_names is None: - if time_setup == 'dt': + if time_setup == "dt": if not final_only: # if len(dQddtlist[0].data) == 0: # J['q','dt'] = np.zeros(J['q','dt'].shape) # else: # J['q','dt'] = dQddtlist[0].data - J['q','dt'] = np.squeeze(dQddtlist[0].toarray()[1:]) + J["q", "dt"] = np.squeeze(dQddtlist[0].toarray()[1:]) # if len(dQddtlist[0].getrow(-1).data) == 0: # J['q_final','dt'] = 0 # else: # J['q_final','dt'] = dQddtlist[0].getrow(-1).data - J['q_final','dt'] = np.squeeze(dQddtlist[0].getrow(-1).toarray()) + J["q_final", "dt"] = np.squeeze(dQddtlist[0].getrow(-1).toarray()) - elif time_setup == 'duration': + elif time_setup == "duration": if not final_only: # if len(dQddtlist[0].data) == 0: # J['q','duration'] = np.zeros(J['q','duration'].shape) # else: # J['q','duration'] = dQddtlist[0].data / (num_nodes - 1) - J['q','duration'] = np.squeeze(dQddtlist[0].toarray()[1:] / (num_nodes - 1)) + J["q", "duration"] = np.squeeze(dQddtlist[0].toarray()[1:] / (num_nodes - 1)) # if len(dQddtlist[0].getrow(-1).data) == 0: # J['q_final','duration'] = 0 # else: # J['q_final','duration'] = dQddtlist[0].getrow(-1).data / (num_nodes - 1) - J['q_final','duration'] = np.squeeze(dQddtlist[0].getrow(-1).toarray() / (num_nodes - 1)) + J["q_final", "duration"] = np.squeeze(dQddtlist[0].getrow(-1).toarray() / (num_nodes - 1)) - elif time_setup == 'bounds': + elif time_setup == "bounds": if not final_only: if len(dQddtlist[0].data) == 0: - J['q','t_initial'] = np.zeros(J['q','t_initial'].shape) - J['q','t_final'] = np.zeros(J['q','t_final'].shape) + J["q", "t_initial"] = np.zeros(J["q", "t_initial"].shape) + J["q", "t_final"] = np.zeros(J["q", "t_final"].shape) else: - J['q','t_initial'] = -dQddtlist[0].data / (num_nodes - 1) - J['q','t_final'] = dQddtlist[0].data / (num_nodes - 1) + J["q", "t_initial"] = -dQddtlist[0].data / (num_nodes - 1) + J["q", "t_final"] = dQddtlist[0].data / (num_nodes - 1) if len(dQddtlist[0].getrow(-1).data) == 0: - J['q_final','t_initial'] = 0 - J['q_final','t_final'] = 0 + J["q_final", "t_initial"] = 0 + J["q_final", "t_final"] = 0 else: - J['q_final','t_initial'] = -dQddtlist[0].getrow(-1).data / (num_nodes - 1) - J['q_final','t_final'] = dQddtlist[0].getrow(-1).data / (num_nodes - 1) + J["q_final", "t_initial"] = -dQddtlist[0].getrow(-1).data / (num_nodes - 1) + J["q_final", "t_final"] = dQddtlist[0].getrow(-1).data / (num_nodes - 1) else: for i_seg, segment_name in enumerate(segment_names): if not final_only: - J['q',segment_name+'|dt'] = dQddtlist[i_seg].data - J['q_final',segment_name+'|dt'] = dQddtlist[i_seg].getrow(-1).data \ No newline at end of file + J["q", segment_name + "|dt"] = dQddtlist[i_seg].data + J["q_final", segment_name + "|dt"] = dQddtlist[i_seg].getrow(-1).data diff --git a/openconcept/utilities/math/max_min_comp.py b/openconcept/utilities/math/max_min_comp.py index 9a8dead9..3d65d9f9 100644 --- a/openconcept/utilities/math/max_min_comp.py +++ b/openconcept/utilities/math/max_min_comp.py @@ -1,6 +1,7 @@ import openmdao.api as om import numpy as np + class MaxComp(om.ExplicitComponent): """ Takes in a vector and outputs a scalar that is the value of the maximum element in the input. @@ -14,7 +15,7 @@ class MaxComp(om.ExplicitComponent): ------- max : same as data type of input array The maximum value of the input array (scalar) - + Options ------- num_nodes : int @@ -22,24 +23,25 @@ class MaxComp(om.ExplicitComponent): units : string OpenMDAO-style units of input and output """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Length of all input and output arrays') - self.options.declare('units', default=None, desc='Units of input array') - + self.options.declare("num_nodes", default=1, desc="Length of all input and output arrays") + self.options.declare("units", default=None, desc="Units of input array") + def setup(self): - nn = self.options['num_nodes'] - unit = self.options['units'] + nn = self.options["num_nodes"] + unit = self.options["units"] + + self.add_input("array", shape=(nn,), units=unit) + self.add_output("max", units=unit) - self.add_input('array', shape=(nn,), units=unit) - self.add_output('max', units=unit) + self.declare_partials("max", "array", rows=np.zeros(nn), cols=np.arange(0, nn)) - self.declare_partials('max', 'array', rows=np.zeros(nn), cols=np.arange(0, nn)) - def compute(self, inputs, outputs): - outputs['max'] = np.amax(inputs['array']) - + outputs["max"] = np.amax(inputs["array"]) + def compute_partials(self, inputs, J): - J['max', 'array'] = np.where(inputs['array'] == np.amax(inputs['array']), 1, 0) + J["max", "array"] = np.where(inputs["array"] == np.amax(inputs["array"]), 1, 0) class MinComp(om.ExplicitComponent): @@ -55,7 +57,7 @@ class MinComp(om.ExplicitComponent): ------- min : same as data type of input array The minimum value of the input array (scalar) - + Options ------- num_nodes : int @@ -63,23 +65,24 @@ class MinComp(om.ExplicitComponent): units : string OpenMDAO-style units of input and output """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Length of all input and output arrays') - self.options.declare('units', default=None, desc='Units of input array') - + self.options.declare("num_nodes", default=1, desc="Length of all input and output arrays") + self.options.declare("units", default=None, desc="Units of input array") + def setup(self): - nn = self.options['num_nodes'] - unit = self.options['units'] + nn = self.options["num_nodes"] + unit = self.options["units"] - self.add_input('array', shape=(nn,), units=unit) - self.add_output('min', units=unit) + self.add_input("array", shape=(nn,), units=unit) + self.add_output("min", units=unit) + + self.declare_partials("min", "array", rows=np.zeros(nn), cols=np.arange(0, nn)) - self.declare_partials('min', 'array', rows=np.zeros(nn), cols=np.arange(0, nn)) - def compute(self, inputs, outputs): - print(inputs['array']) - outputs['min'] = np.amin(inputs['array']) - print(outputs['min']) - + print(inputs["array"]) + outputs["min"] = np.amin(inputs["array"]) + print(outputs["min"]) + def compute_partials(self, inputs, J): - J['min', 'array'] = np.where(inputs['array'] == np.amin(inputs['array']), 1, 0) \ No newline at end of file + J["min", "array"] = np.where(inputs["array"] == np.amin(inputs["array"]), 1, 0) diff --git a/openconcept/utilities/math/multiply_divide_comp.py b/openconcept/utilities/math/multiply_divide_comp.py index bf952ae9..f41e125e 100644 --- a/openconcept/utilities/math/multiply_divide_comp.py +++ b/openconcept/utilities/math/multiply_divide_comp.py @@ -36,8 +36,18 @@ class ElementMultiplyDivideComp(ExplicitComponent): List of equation systems to be initialized with the system. """ - def __init__(self, output_name=None, input_names=None, vec_size=1, length=1, - val=1.0, scaling_factor=1, divide=None, input_units=None, **kwargs): + def __init__( + self, + output_name=None, + input_names=None, + vec_size=1, + length=1, + val=1.0, + scaling_factor=1, + divide=None, + input_units=None, + **kwargs + ): """ Allow user to create an multiplication system with one-liner. @@ -78,24 +88,40 @@ def __init__(self, output_name=None, input_names=None, vec_size=1, length=1, self._add_systems = [] if isinstance(output_name, str): - self._add_systems.append((output_name, input_names, vec_size, length, val, - scaling_factor, divide, input_units, kwargs)) + self._add_systems.append( + (output_name, input_names, vec_size, length, val, scaling_factor, divide, input_units, kwargs) + ) elif isinstance(output_name, Iterable): - raise NotImplementedError('Declaring multiple systems ' - 'on initiation is not implemented.' - 'Use a string to name a single addition relationship or use ' - 'multiple add_equation calls') + raise NotImplementedError( + "Declaring multiple systems " + "on initiation is not implemented." + "Use a string to name a single addition relationship or use " + "multiple add_equation calls" + ) elif output_name is None: pass else: - raise ValueError( - "first argument to init must be either of type " - "`str' or 'None'") - - def add_equation(self, output_name, input_names, vec_size=1, length=1, val=1.0, - res_units=None, desc='', lower=None, upper=None, ref=1.0, - ref0=0.0, res_ref=None, scaling_factor=1, - divide=None, input_units=None, tags=None): + raise ValueError("first argument to init must be either of type " "`str' or 'None'") + + def add_equation( + self, + output_name, + input_names, + vec_size=1, + length=1, + val=1.0, + res_units=None, + desc="", + lower=None, + upper=None, + ref=1.0, + ref0=0.0, + res_ref=None, + scaling_factor=1, + divide=None, + input_units=None, + tags=None, + ): """ Add a multiplication relation. @@ -153,28 +179,46 @@ def add_equation(self, output_name, input_names, vec_size=1, length=1, val=1.0, tags : list of str Tags to apply to the output variable """ - kwargs = {'res_units': res_units, 'desc': desc, - 'lower': lower, 'upper': upper, 'ref': ref, 'ref0': ref0, - 'res_ref': res_ref, 'tags': tags} - self._add_systems.append((output_name, input_names, vec_size, length, val, - scaling_factor, divide, input_units, kwargs)) + kwargs = { + "res_units": res_units, + "desc": desc, + "lower": lower, + "upper": upper, + "ref": ref, + "ref0": ref0, + "res_ref": res_ref, + "tags": tags, + } + self._add_systems.append( + (output_name, input_names, vec_size, length, val, scaling_factor, divide, input_units, kwargs) + ) def add_output(self): """ Use add_equation instead of add_output to define equation systems. """ - raise NotImplementedError('Use add_equation method, not add_output method' - 'to create an multliplication/division relation') + raise NotImplementedError( + "Use add_equation method, not add_output method" "to create an multliplication/division relation" + ) def setup(self): """ Set up the addition/subtraction system at run time. """ - for (output_name, input_names, vec_size, length, val, - scaling_factor, divide, input_units, kwargs) in self._add_systems: + for ( + output_name, + input_names, + vec_size, + length, + val, + scaling_factor, + divide, + input_units, + kwargs, + ) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] - desc = kwargs.get('desc', '') + desc = kwargs.get("desc", "") if divide is None: divide = [False for k in range(len(input_names))] @@ -182,22 +226,20 @@ def setup(self): input_units = [None for k in range(len(input_names))] if len(divide) != len(input_names): - raise ValueError('Division bool list needs to be same length as input names') + raise ValueError("Division bool list needs to be same length as input names") if len(input_units) != len(input_names): - raise ValueError('Input units list needs to be same length as input names') + raise ValueError("Input units list needs to be same length as input names") if isinstance(vec_size, Iterable): # scalar - vector mutliplication multi_vec_size = True if len(vec_size) != len(input_names): - raise ValueError('Inputs list needs to be same length as vec_sizes list') + raise ValueError("Inputs list needs to be same length as vec_sizes list") vec_out_size = max(vec_size) else: multi_vec_size = False vec_out_size = vec_size - - output_units_assemble = [] for i, input_name in enumerate(input_names): @@ -211,8 +253,7 @@ def setup(self): else: shape = (vec_in_size, length) - self.add_input(input_name, shape=shape, units=input_units[i], - desc=desc + '_inp_' + input_name) + self.add_input(input_name, shape=shape, units=input_units[i], desc=desc + "_inp_" + input_name) if vec_in_size == 1: # scalar input @@ -220,22 +261,22 @@ def setup(self): else: # vector input col_vals = np.arange(0, vec_out_size * length) - self.declare_partials([output_name], [input_name], - cols=col_vals, - rows=np.arange(0, vec_out_size * length)) + self.declare_partials( + [output_name], [input_name], cols=col_vals, rows=np.arange(0, vec_out_size * length) + ) # derive the units of the output vector from the inputs if input_units[i] is not None: if divide[i]: if i == 0: - output_units_assemble.append('(' + input_units[i] + ')**-1 ') + output_units_assemble.append("(" + input_units[i] + ")**-1 ") else: - output_units_assemble.append('/ (' + input_units[i] + ') ') + output_units_assemble.append("/ (" + input_units[i] + ") ") else: if i == 0: - output_units_assemble.append(input_units[i] + ' ') + output_units_assemble.append(input_units[i] + " ") else: - output_units_assemble.append('* (' + input_units[i] + ') ') - output_units = ''.join(output_units_assemble) + output_units_assemble.append("* (" + input_units[i] + ") ") + output_units = "".join(output_units_assemble) if len(output_units_assemble) == 0: output_units = None @@ -244,9 +285,9 @@ def setup(self): else: out_shape = (vec_out_size, length) - super(ElementMultiplyDivideComp, self).add_output(output_name, val, - shape=out_shape, units=output_units, - **kwargs) + super(ElementMultiplyDivideComp, self).add_output( + output_name, val, shape=out_shape, units=output_units, **kwargs + ) def compute(self, inputs, outputs): """ @@ -259,8 +300,17 @@ def compute(self, inputs, outputs): outputs : Vector unscaled, dimensional output variables read via outputs[key] """ - for (output_name, input_names, vec_size, length, val, scaling_factor, divide, - input_units, kwargs) in self._add_systems: + for ( + output_name, + input_names, + vec_size, + length, + val, + scaling_factor, + divide, + input_units, + kwargs, + ) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] @@ -292,8 +342,17 @@ def compute(self, inputs, outputs): outputs[output_name] = temp * scaling_factor def compute_partials(self, inputs, J): - for (output_name, input_names, vec_size, length, val, scaling_factor, divide, - input_units, kwargs) in self._add_systems: + for ( + output_name, + input_names, + vec_size, + length, + val, + scaling_factor, + divide, + input_units, + kwargs, + ) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] @@ -321,7 +380,7 @@ def compute_partials(self, inputs, J): else: # if i is the differentiated variable if divide[i]: - temp = - temp / inputs[input_name_partial] ** 2 + temp = -temp / inputs[input_name_partial] ** 2 else: pass temp = temp * scaling_factor diff --git a/openconcept/utilities/math/tests/test_add_subtract_comp.py b/openconcept/utilities/math/tests/test_add_subtract_comp.py index 9e1ca1d8..2256e158 100644 --- a/openconcept/utilities/math/tests/test_add_subtract_comp.py +++ b/openconcept/utilities/math/tests/test_add_subtract_comp.py @@ -8,325 +8,318 @@ from openconcept.utilities import AddSubtractComp from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -class TestAddSubtractCompScalars(unittest.TestCase): +class TestAddSubtractCompScalars(unittest.TestCase): def setUp(self): self.nn = 1 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - adder=self.p.model.add_subsystem(name='add_subtract_comp', - subsys=AddSubtractComp()) - adder.add_equation('adder_output',['input_a','input_b']) + adder = self.p.model.add_subsystem(name="add_subtract_comp", subsys=AddSubtractComp()) + adder.add_equation("adder_output", ["input_a", "input_b"]) - self.p.model.connect('a', 'add_subtract_comp.input_a') - self.p.model.connect('b', 'add_subtract_comp.input_b') + self.p.model.connect("a", "add_subtract_comp.input_a") + self.p.model.connect("b", "add_subtract_comp.input_b") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,) - self.p['b'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) + self.p["b"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['add_subtract_comp.adder_output'] + a = self.p["a"] + b = self.p["b"] + out = self.p["add_subtract_comp.adder_output"] expected = a + b - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestAddSubtractCompNx1(unittest.TestCase): +class TestAddSubtractCompNx1(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - adder=self.p.model.add_subsystem(name='add_subtract_comp', - subsys=AddSubtractComp()) - adder.add_equation('adder_output',['input_a','input_b'],vec_size=self.nn) + adder = self.p.model.add_subsystem(name="add_subtract_comp", subsys=AddSubtractComp()) + adder.add_equation("adder_output", ["input_a", "input_b"], vec_size=self.nn) - self.p.model.connect('a', 'add_subtract_comp.input_a') - self.p.model.connect('b', 'add_subtract_comp.input_b') + self.p.model.connect("a", "add_subtract_comp.input_a") + self.p.model.connect("b", "add_subtract_comp.input_b") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,) - self.p['b'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) + self.p["b"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['add_subtract_comp.adder_output'] + a = self.p["a"] + b = self.p["b"] + out = self.p["add_subtract_comp.adder_output"] expected = a + b - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestAddSubtractCompNx1VectorScalar(unittest.TestCase): +class TestAddSubtractCompNx1VectorScalar(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', val=3.0) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", val=3.0) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - adder=self.p.model.add_subsystem(name='add_subtract_comp', - subsys=AddSubtractComp()) - adder.add_equation('adder_output',['input_a','input_b'],vec_size=[self.nn,1]) + adder = self.p.model.add_subsystem(name="add_subtract_comp", subsys=AddSubtractComp()) + adder.add_equation("adder_output", ["input_a", "input_b"], vec_size=[self.nn, 1]) - self.p.model.connect('a', 'add_subtract_comp.input_a') - self.p.model.connect('b', 'add_subtract_comp.input_b') + self.p.model.connect("a", "add_subtract_comp.input_a") + self.p.model.connect("b", "add_subtract_comp.input_b") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['add_subtract_comp.adder_output'] + a = self.p["a"] + b = self.p["b"] + out = self.p["add_subtract_comp.adder_output"] expected = a + b - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestAddSubtractCompNx3(unittest.TestCase): +class TestAddSubtractCompNx3(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - adder=self.p.model.add_subsystem(name='add_subtract_comp', - subsys=AddSubtractComp()) - adder.add_equation('adder_output',['input_a','input_b'],vec_size=self.nn,length=3) + adder = self.p.model.add_subsystem(name="add_subtract_comp", subsys=AddSubtractComp()) + adder.add_equation("adder_output", ["input_a", "input_b"], vec_size=self.nn, length=3) - self.p.model.connect('a', 'add_subtract_comp.input_a') - self.p.model.connect('b', 'add_subtract_comp.input_b') + self.p.model.connect("a", "add_subtract_comp.input_a") + self.p.model.connect("b", "add_subtract_comp.input_b") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn, 3) - self.p['b'] = np.random.rand(self.nn, 3) + self.p["a"] = np.random.rand(self.nn, 3) + self.p["b"] = np.random.rand(self.nn, 3) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['add_subtract_comp.adder_output'] + a = self.p["a"] + b = self.p["b"] + out = self.p["add_subtract_comp.adder_output"] expected = a + b - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestAddSubtractMultipleInputs(unittest.TestCase): +class TestAddSubtractMultipleInputs(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) - ivc.add_output(name='c', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) + ivc.add_output(name="c", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - adder=self.p.model.add_subsystem(name='add_subtract_comp', - subsys=AddSubtractComp()) - adder.add_equation('adder_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3) + adder = self.p.model.add_subsystem(name="add_subtract_comp", subsys=AddSubtractComp()) + adder.add_equation("adder_output", ["input_a", "input_b", "input_c"], vec_size=self.nn, length=3) - self.p.model.connect('a', 'add_subtract_comp.input_a') - self.p.model.connect('b', 'add_subtract_comp.input_b') - self.p.model.connect('c', 'add_subtract_comp.input_c') + self.p.model.connect("a", "add_subtract_comp.input_a") + self.p.model.connect("b", "add_subtract_comp.input_b") + self.p.model.connect("c", "add_subtract_comp.input_c") self.p.setup(force_alloc_complex=False) - self.p['a'] = np.random.rand(self.nn, 3) - self.p['b'] = np.random.rand(self.nn, 3) - self.p['c'] = np.random.rand(self.nn, 3) + self.p["a"] = np.random.rand(self.nn, 3) + self.p["b"] = np.random.rand(self.nn, 3) + self.p["c"] = np.random.rand(self.nn, 3) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - out = self.p['add_subtract_comp.adder_output'] + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + out = self.p["add_subtract_comp.adder_output"] expected = a + b + c - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestAddSubtractScalingFactors(unittest.TestCase): +class TestAddSubtractScalingFactors(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) - ivc.add_output(name='c', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) + ivc.add_output(name="c", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - adder=self.p.model.add_subsystem(name='add_subtract_comp', - subsys=AddSubtractComp()) - adder.add_equation('adder_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3,scaling_factors=[2.,1.,-1]) + adder = self.p.model.add_subsystem(name="add_subtract_comp", subsys=AddSubtractComp()) + adder.add_equation( + "adder_output", + ["input_a", "input_b", "input_c"], + vec_size=self.nn, + length=3, + scaling_factors=[2.0, 1.0, -1], + ) - self.p.model.connect('a', 'add_subtract_comp.input_a') - self.p.model.connect('b', 'add_subtract_comp.input_b') - self.p.model.connect('c', 'add_subtract_comp.input_c') + self.p.model.connect("a", "add_subtract_comp.input_a") + self.p.model.connect("b", "add_subtract_comp.input_b") + self.p.model.connect("c", "add_subtract_comp.input_c") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn, 3) - self.p['b'] = np.random.rand(self.nn, 3) - self.p['c'] = np.random.rand(self.nn, 3) + self.p["a"] = np.random.rand(self.nn, 3) + self.p["b"] = np.random.rand(self.nn, 3) + self.p["c"] = np.random.rand(self.nn, 3) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - out = self.p['add_subtract_comp.adder_output'] - expected = 2*a + b - c - assert_near_equal(out, expected,1e-16) + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + out = self.p["add_subtract_comp.adder_output"] + expected = 2 * a + b - c + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestAddSubtractUnits(unittest.TestCase): +class TestAddSubtractUnits(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3), units='ft') - ivc.add_output(name='b', shape=(self.nn, 3), units='m') - ivc.add_output(name='c', shape=(self.nn, 3), units='m') + ivc.add_output(name="a", shape=(self.nn, 3), units="ft") + ivc.add_output(name="b", shape=(self.nn, 3), units="m") + ivc.add_output(name="c", shape=(self.nn, 3), units="m") - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - adder=self.p.model.add_subsystem(name='add_subtract_comp', - subsys=AddSubtractComp()) - adder.add_equation('adder_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3, units='ft') + adder = self.p.model.add_subsystem(name="add_subtract_comp", subsys=AddSubtractComp()) + adder.add_equation("adder_output", ["input_a", "input_b", "input_c"], vec_size=self.nn, length=3, units="ft") - self.p.model.connect('a', 'add_subtract_comp.input_a') - self.p.model.connect('b', 'add_subtract_comp.input_b') - self.p.model.connect('c', 'add_subtract_comp.input_c') + self.p.model.connect("a", "add_subtract_comp.input_a") + self.p.model.connect("b", "add_subtract_comp.input_b") + self.p.model.connect("c", "add_subtract_comp.input_c") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn, 3) - self.p['b'] = np.random.rand(self.nn, 3) - self.p['c'] = np.random.rand(self.nn, 3) + self.p["a"] = np.random.rand(self.nn, 3) + self.p["b"] = np.random.rand(self.nn, 3) + self.p["c"] = np.random.rand(self.nn, 3) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - out = self.p['add_subtract_comp.adder_output'] + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + out = self.p["add_subtract_comp.adder_output"] m_to_ft = 3.280839895 - expected = a + b*m_to_ft + c*m_to_ft - assert_near_equal(out, expected,1e-8) + expected = a + b * m_to_ft + c * m_to_ft + assert_near_equal(out, expected, 1e-8) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestWrongScalingFactorCount(unittest.TestCase): +class TestWrongScalingFactorCount(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) - ivc.add_output(name='c', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) + ivc.add_output(name="c", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - adder=self.p.model.add_subsystem(name='add_subtract_comp', - subsys=AddSubtractComp()) - adder.add_equation('adder_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3,scaling_factors=[1,-1]) - - self.p.model.connect('a', 'add_subtract_comp.input_a') - self.p.model.connect('b', 'add_subtract_comp.input_b') - self.p.model.connect('c', 'add_subtract_comp.input_c') + adder = self.p.model.add_subsystem(name="add_subtract_comp", subsys=AddSubtractComp()) + adder.add_equation( + "adder_output", ["input_a", "input_b", "input_c"], vec_size=self.nn, length=3, scaling_factors=[1, -1] + ) + self.p.model.connect("a", "add_subtract_comp.input_a") + self.p.model.connect("b", "add_subtract_comp.input_b") + self.p.model.connect("c", "add_subtract_comp.input_c") def test_for_exception(self): - self.assertRaises(ValueError,self.p.setup) + self.assertRaises(ValueError, self.p.setup) -class TestForDocs(unittest.TestCase): +class TestForDocs(unittest.TestCase): def test(self): """ A simple example to compute the resultant force on an aircraft and demonstrate the AddSubtract component @@ -340,34 +333,39 @@ def test(self): p = Problem(model=Group()) ivc = IndepVarComp() - #the vector represents forces at 3 analysis points (rows) in 2 dimensional plane (cols) - ivc.add_output(name='thrust', shape=(n,2), units='kN') - ivc.add_output(name='drag', shape=(n,2), units='kN') - ivc.add_output(name='lift', shape=(n,2), units='kN') - ivc.add_output(name='weight', shape=(n,2), units='kN') - p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['thrust', 'drag', 'lift', 'weight']) - - #construct an adder/subtracter here. create a relationship through the add_equation method + # the vector represents forces at 3 analysis points (rows) in 2 dimensional plane (cols) + ivc.add_output(name="thrust", shape=(n, 2), units="kN") + ivc.add_output(name="drag", shape=(n, 2), units="kN") + ivc.add_output(name="lift", shape=(n, 2), units="kN") + ivc.add_output(name="weight", shape=(n, 2), units="kN") + p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["thrust", "drag", "lift", "weight"]) + + # construct an adder/subtracter here. create a relationship through the add_equation method adder = AddSubtractComp() - adder.add_equation('total_force',input_names=['thrust','drag','lift','weight'],vec_size=n,length=2, scaling_factors=[1,-1,1,-1], units='kN') - #note the scaling factors. we assume all forces are positive sign upstream - - p.model.add_subsystem(name='totalforcecomp', subsys=adder) - - p.model.connect('thrust', 'totalforcecomp.thrust') - p.model.connect('drag', 'totalforcecomp.drag') - p.model.connect('lift', 'totalforcecomp.lift') - p.model.connect('weight', 'totalforcecomp.weight') + adder.add_equation( + "total_force", + input_names=["thrust", "drag", "lift", "weight"], + vec_size=n, + length=2, + scaling_factors=[1, -1, 1, -1], + units="kN", + ) + # note the scaling factors. we assume all forces are positive sign upstream + + p.model.add_subsystem(name="totalforcecomp", subsys=adder) + + p.model.connect("thrust", "totalforcecomp.thrust") + p.model.connect("drag", "totalforcecomp.drag") + p.model.connect("lift", "totalforcecomp.lift") + p.model.connect("weight", "totalforcecomp.weight") p.setup(force_alloc_complex=True) - #set thrust to exceed drag, weight to equal lift for this scenario - p['thrust'][:,0] = [500, 600, 700] - p['drag'][:,0] = [400, 400, 400] - p['weight'][:,1] = [1000, 1001, 1002] - p['lift'][:,1] = [1000, 1000, 1000] + # set thrust to exceed drag, weight to equal lift for this scenario + p["thrust"][:, 0] = [500, 600, 700] + p["drag"][:, 0] = [400, 400, 400] + p["weight"][:, 1] = [1000, 1001, 1002] + p["lift"][:, 1] = [1000, 1000, 1000] p.run_model() @@ -375,8 +373,8 @@ def test(self): # Verify the results expected_i = np.array([[100, 200, 300], [0, -1, -2]]).T - assert_near_equal(p.get_val('totalforcecomp.total_force', units='kN'), expected_i) + assert_near_equal(p.get_val("totalforcecomp.total_force", units="kN"), expected_i) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/openconcept/utilities/math/tests/test_combine_split.py b/openconcept/utilities/math/tests/test_combine_split.py index 4624ccd4..7a8abb1c 100644 --- a/openconcept/utilities/math/tests/test_combine_split.py +++ b/openconcept/utilities/math/tests/test_combine_split.py @@ -8,590 +8,586 @@ from openconcept.utilities import VectorConcatenateComp, VectorSplitComp from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -class TestConcatenateScalars(unittest.TestCase): +class TestConcatenateScalars(unittest.TestCase): def setUp(self): self.nn = 1 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp()) - combiner.add_relation('concat_output',['input_a','input_b'],vec_sizes=[1,1]) + combiner = self.p.model.add_subsystem(name="vector_concat_comp", subsys=VectorConcatenateComp()) + combiner.add_relation("concat_output", ["input_a", "input_b"], vec_sizes=[1, 1]) - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,) - self.p['b'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) + self.p["b"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['vector_concat_comp.concat_output'] - expected = np.concatenate((a,b)) - assert_near_equal(out, expected,1e-16) + a = self.p["a"] + b = self.p["b"] + out = self.p["vector_concat_comp.concat_output"] + expected = np.concatenate((a, b)) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestConcatenateNx1(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp()) - combiner.add_relation('concat_output',['input_a','input_b'],vec_sizes=[self.nn,self.nn]) + combiner = self.p.model.add_subsystem(name="vector_concat_comp", subsys=VectorConcatenateComp()) + combiner.add_relation("concat_output", ["input_a", "input_b"], vec_sizes=[self.nn, self.nn]) - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,) - self.p['b'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) + self.p["b"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['vector_concat_comp.concat_output'] - expected = np.concatenate((a,b)) - assert_near_equal(out, expected,1e-16) + a = self.p["a"] + b = self.p["b"] + out = self.p["vector_concat_comp.concat_output"] + expected = np.concatenate((a, b)) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestConcatenateNx3(unittest.TestCase): def setUp(self): self.nn = 5 self.length = 3 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,self.length)) - ivc.add_output(name='b', shape=(self.nn,self.length)) + ivc.add_output(name="a", shape=(self.nn, self.length)) + ivc.add_output(name="b", shape=(self.nn, self.length)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp()) - combiner.add_relation('concat_output',['input_a','input_b'],vec_sizes=[self.nn,self.nn],length=self.length) + combiner = self.p.model.add_subsystem(name="vector_concat_comp", subsys=VectorConcatenateComp()) + combiner.add_relation("concat_output", ["input_a", "input_b"], vec_sizes=[self.nn, self.nn], length=self.length) - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") self.p.setup(force_alloc_complex=False) - self.p['a'] = np.random.rand(self.nn,self.length) - self.p['b'] = np.random.rand(self.nn,self.length) + self.p["a"] = np.random.rand(self.nn, self.length) + self.p["b"] = np.random.rand(self.nn, self.length) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['vector_concat_comp.concat_output'] - expected = np.concatenate((a,b)) - assert_near_equal(out, expected,1e-16) + a = self.p["a"] + b = self.p["b"] + out = self.p["vector_concat_comp.concat_output"] + expected = np.concatenate((a, b)) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) + class TestConcatenateInitMethod(unittest.TestCase): def setUp(self): self.nn = 5 self.length = 3 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,self.length)) - ivc.add_output(name='b', shape=(self.nn,self.length)) + ivc.add_output(name="a", shape=(self.nn, self.length)) + ivc.add_output(name="b", shape=(self.nn, self.length)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp('concat_output',['input_a','input_b'],vec_sizes=[self.nn,self.nn],length=self.length)) - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') + combiner = self.p.model.add_subsystem( + name="vector_concat_comp", + subsys=VectorConcatenateComp( + "concat_output", ["input_a", "input_b"], vec_sizes=[self.nn, self.nn], length=self.length + ), + ) + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,self.length) - self.p['b'] = np.random.rand(self.nn,self.length) + self.p["a"] = np.random.rand(self.nn, self.length) + self.p["b"] = np.random.rand(self.nn, self.length) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['vector_concat_comp.concat_output'] - expected = np.concatenate((a,b)) - assert_near_equal(out, expected,1e-16) + a = self.p["a"] + b = self.p["b"] + out = self.p["vector_concat_comp.concat_output"] + expected = np.concatenate((a, b)) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestConcatenateMultipleSystems(unittest.TestCase): def setUp(self): self.nn = 5 self.length = 3 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,self.length)) - ivc.add_output(name='b', shape=(self.nn,self.length)) - ivc.add_output(name='c', shape=(3,)) - ivc.add_output(name='d', shape=(4,)) - - - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c','d']) + ivc.add_output(name="a", shape=(self.nn, self.length)) + ivc.add_output(name="b", shape=(self.nn, self.length)) + ivc.add_output(name="c", shape=(3,)) + ivc.add_output(name="d", shape=(4,)) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp()) - combiner.add_relation('concat_output1',['input_a','input_b'],vec_sizes=[self.nn,self.nn],length=self.length) - combiner.add_relation('concat_output2',['input_c','input_d'],vec_sizes=[3,4]) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c", "d"]) - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') - self.p.model.connect('c', 'vector_concat_comp.input_c') - self.p.model.connect('d', 'vector_concat_comp.input_d') + combiner = self.p.model.add_subsystem(name="vector_concat_comp", subsys=VectorConcatenateComp()) + combiner.add_relation( + "concat_output1", ["input_a", "input_b"], vec_sizes=[self.nn, self.nn], length=self.length + ) + combiner.add_relation("concat_output2", ["input_c", "input_d"], vec_sizes=[3, 4]) + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") + self.p.model.connect("c", "vector_concat_comp.input_c") + self.p.model.connect("d", "vector_concat_comp.input_d") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,self.length) - self.p['b'] = np.random.rand(self.nn,self.length) - self.p['c'] = np.random.rand(3,) - self.p['d'] = np.random.rand(4,) + self.p["a"] = np.random.rand(self.nn, self.length) + self.p["b"] = np.random.rand(self.nn, self.length) + self.p["c"] = np.random.rand( + 3, + ) + self.p["d"] = np.random.rand( + 4, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - d = self.p['d'] + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + d = self.p["d"] - out1 = self.p['vector_concat_comp.concat_output1'] - out2 = self.p['vector_concat_comp.concat_output2'] + out1 = self.p["vector_concat_comp.concat_output1"] + out2 = self.p["vector_concat_comp.concat_output2"] - expected1 = np.concatenate((a,b)) - expected2 = np.concatenate((c,d)) - assert_near_equal(out1, expected1,1e-16) - assert_near_equal(out2, expected2,1e-16) + expected1 = np.concatenate((a, b)) + expected2 = np.concatenate((c, d)) + assert_near_equal(out1, expected1, 1e-16) + assert_near_equal(out2, expected2, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestConcatenateNx3Units(unittest.TestCase): def setUp(self): self.nn = 5 self.length = 3 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,self.length), units='m') - ivc.add_output(name='b', shape=(self.nn,self.length), units='km') + ivc.add_output(name="a", shape=(self.nn, self.length), units="m") + ivc.add_output(name="b", shape=(self.nn, self.length), units="km") - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp()) - combiner.add_relation('concat_output',['input_a','input_b'],vec_sizes=[self.nn,self.nn],length=self.length, units='m') + combiner = self.p.model.add_subsystem(name="vector_concat_comp", subsys=VectorConcatenateComp()) + combiner.add_relation( + "concat_output", ["input_a", "input_b"], vec_sizes=[self.nn, self.nn], length=self.length, units="m" + ) - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,self.length) - self.p['b'] = np.random.rand(self.nn,self.length) + self.p["a"] = np.random.rand(self.nn, self.length) + self.p["b"] = np.random.rand(self.nn, self.length) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - b = b*1000. - out = self.p['vector_concat_comp.concat_output'] - expected = np.concatenate((a,b)) - assert_near_equal(out, expected,1e-16) + a = self.p["a"] + b = self.p["b"] + b = b * 1000.0 + out = self.p["vector_concat_comp.concat_output"] + expected = np.concatenate((a, b)) + assert_near_equal(out, expected, 1e-16) + class TestConcatenate3InputsDiffSizesNx1(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) - ivc.add_output(name='c', shape=(3,)) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) + ivc.add_output(name="c", shape=(3,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp()) - combiner.add_relation('concat_output',['input_a','input_b','input_c'],vec_sizes=[self.nn,self.nn,3]) - - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') - self.p.model.connect('c', 'vector_concat_comp.input_c') + combiner = self.p.model.add_subsystem(name="vector_concat_comp", subsys=VectorConcatenateComp()) + combiner.add_relation("concat_output", ["input_a", "input_b", "input_c"], vec_sizes=[self.nn, self.nn, 3]) + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") + self.p.model.connect("c", "vector_concat_comp.input_c") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,) - self.p['b'] = np.random.rand(self.nn,) - self.p['c'] = np.random.rand(3,) + self.p["a"] = np.random.rand( + self.nn, + ) + self.p["b"] = np.random.rand( + self.nn, + ) + self.p["c"] = np.random.rand( + 3, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] - out = self.p['vector_concat_comp.concat_output'] - expected = np.concatenate((a,b,c)) - assert_near_equal(out, expected,1e-16) + out = self.p["vector_concat_comp.concat_output"] + expected = np.concatenate((a, b, c)) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestConcatenate3InputsDiffSizesNx3(unittest.TestCase): def setUp(self): self.nn = 5 self.length = 3 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,self.length)) - ivc.add_output(name='b', shape=(self.nn,self.length)) - ivc.add_output(name='c', shape=(3,self.length)) + ivc.add_output(name="a", shape=(self.nn, self.length)) + ivc.add_output(name="b", shape=(self.nn, self.length)) + ivc.add_output(name="c", shape=(3, self.length)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp()) - combiner.add_relation('concat_output',['input_a','input_b','input_c'],vec_sizes=[self.nn,self.nn,3],length=self.length) - - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') - self.p.model.connect('c', 'vector_concat_comp.input_c') + combiner = self.p.model.add_subsystem(name="vector_concat_comp", subsys=VectorConcatenateComp()) + combiner.add_relation( + "concat_output", ["input_a", "input_b", "input_c"], vec_sizes=[self.nn, self.nn, 3], length=self.length + ) + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") + self.p.model.connect("c", "vector_concat_comp.input_c") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.rand(self.nn,self.length) - self.p['b'] = np.random.rand(self.nn,self.length) - self.p['c'] = np.random.rand(3,self.length) + self.p["a"] = np.random.rand(self.nn, self.length) + self.p["b"] = np.random.rand(self.nn, self.length) + self.p["c"] = np.random.rand(3, self.length) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] - out = self.p['vector_concat_comp.concat_output'] - expected = np.concatenate((a,b,c)) - assert_near_equal(out, expected,1e-16) + out = self.p["vector_concat_comp.concat_output"] + expected = np.concatenate((a, b, c)) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) + class TestConcatenateWrongVecSizesInputMismatch(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) - ivc.add_output(name='c', shape=(3,)) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) + ivc.add_output(name="c", shape=(3,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - combiner=self.p.model.add_subsystem(name='vector_concat_comp', - subsys=VectorConcatenateComp()) - combiner.add_relation('concat_output',['input_a','input_b','input_c'],vec_sizes=[self.nn,self.nn]) + combiner = self.p.model.add_subsystem(name="vector_concat_comp", subsys=VectorConcatenateComp()) + combiner.add_relation("concat_output", ["input_a", "input_b", "input_c"], vec_sizes=[self.nn, self.nn]) - self.p.model.connect('a', 'vector_concat_comp.input_a') - self.p.model.connect('b', 'vector_concat_comp.input_b') - self.p.model.connect('c', 'vector_concat_comp.input_c') + self.p.model.connect("a", "vector_concat_comp.input_a") + self.p.model.connect("b", "vector_concat_comp.input_b") + self.p.model.connect("c", "vector_concat_comp.input_c") def test_for_exception(self): - self.assertRaises(ValueError,self.p.setup) + self.assertRaises(ValueError, self.p.setup) -class TestSplitScalars(unittest.TestCase): +class TestSplitScalars(unittest.TestCase): def setUp(self): self.nn = 1 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='input_to_split', shape=(self.nn*2,)) + ivc.add_output(name="input_to_split", shape=(self.nn * 2,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['input_to_split']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["input_to_split"]) - splitter=self.p.model.add_subsystem(name='vector_split_comp', - subsys=VectorSplitComp()) - splitter.add_relation(['output_a','output_b'],'input_to_split',vec_sizes=[1,1]) + splitter = self.p.model.add_subsystem(name="vector_split_comp", subsys=VectorSplitComp()) + splitter.add_relation(["output_a", "output_b"], "input_to_split", vec_sizes=[1, 1]) - self.p.model.connect('input_to_split', 'vector_split_comp.input_to_split') + self.p.model.connect("input_to_split", "vector_split_comp.input_to_split") self.p.setup(force_alloc_complex=True) - self.p['input_to_split'] = np.random.rand(self.nn*2,) + self.p["input_to_split"] = np.random.rand( + self.nn * 2, + ) self.p.run_model() def test_results(self): - input_to_split = self.p['input_to_split'] - out_a = self.p['vector_split_comp.output_a'] - out_b = self.p['vector_split_comp.output_b'] + input_to_split = self.p["input_to_split"] + out_a = self.p["vector_split_comp.output_a"] + out_b = self.p["vector_split_comp.output_b"] expected_a = input_to_split[0] expected_b = input_to_split[1] - assert_near_equal(out_a, expected_a,1e-16) - assert_near_equal(out_b, expected_b,1e-16) + assert_near_equal(out_a, expected_a, 1e-16) + assert_near_equal(out_b, expected_b, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestSplitNx1(unittest.TestCase): +class TestSplitNx1(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='input_to_split', shape=(self.nn*2,)) + ivc.add_output(name="input_to_split", shape=(self.nn * 2,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['input_to_split']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["input_to_split"]) - splitter=self.p.model.add_subsystem(name='vector_split_comp', - subsys=VectorSplitComp()) - splitter.add_relation(['output_a','output_b'],'input_to_split',vec_sizes=[self.nn,self.nn]) + splitter = self.p.model.add_subsystem(name="vector_split_comp", subsys=VectorSplitComp()) + splitter.add_relation(["output_a", "output_b"], "input_to_split", vec_sizes=[self.nn, self.nn]) - self.p.model.connect('input_to_split', 'vector_split_comp.input_to_split') + self.p.model.connect("input_to_split", "vector_split_comp.input_to_split") self.p.setup(force_alloc_complex=True) - self.p['input_to_split'] = np.random.rand(self.nn*2,) + self.p["input_to_split"] = np.random.rand( + self.nn * 2, + ) self.p.run_model() def test_results(self): - input_to_split = self.p['input_to_split'] - out_a = self.p['vector_split_comp.output_a'] - out_b = self.p['vector_split_comp.output_b'] + input_to_split = self.p["input_to_split"] + out_a = self.p["vector_split_comp.output_a"] + out_b = self.p["vector_split_comp.output_b"] - expected_a = input_to_split[0:self.nn] - expected_b = input_to_split[self.nn:2*self.nn] - assert_near_equal(out_a, expected_a,1e-16) - assert_near_equal(out_b, expected_b,1e-16) + expected_a = input_to_split[0 : self.nn] + expected_b = input_to_split[self.nn : 2 * self.nn] + assert_near_equal(out_a, expected_a, 1e-16) + assert_near_equal(out_b, expected_b, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestSplitNx3(unittest.TestCase): +class TestSplitNx3(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='input_to_split', shape=(self.nn*2,3)) + ivc.add_output(name="input_to_split", shape=(self.nn * 2, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['input_to_split']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["input_to_split"]) - splitter=self.p.model.add_subsystem(name='vector_split_comp', - subsys=VectorSplitComp()) - splitter.add_relation(['output_a','output_b'],'input_to_split',vec_sizes=[self.nn,self.nn],length=3) + splitter = self.p.model.add_subsystem(name="vector_split_comp", subsys=VectorSplitComp()) + splitter.add_relation(["output_a", "output_b"], "input_to_split", vec_sizes=[self.nn, self.nn], length=3) - self.p.model.connect('input_to_split', 'vector_split_comp.input_to_split') + self.p.model.connect("input_to_split", "vector_split_comp.input_to_split") self.p.setup(force_alloc_complex=True) - self.p['input_to_split'] = np.random.rand(self.nn*2,3) + self.p["input_to_split"] = np.random.rand(self.nn * 2, 3) self.p.run_model() def test_results(self): - input_to_split = self.p['input_to_split'] - out_a = self.p['vector_split_comp.output_a'] - out_b = self.p['vector_split_comp.output_b'] + input_to_split = self.p["input_to_split"] + out_a = self.p["vector_split_comp.output_a"] + out_b = self.p["vector_split_comp.output_b"] - expected_a = input_to_split[0:self.nn,:] - expected_b = input_to_split[self.nn:2*self.nn,:] - assert_near_equal(out_a, expected_a,1e-16) - assert_near_equal(out_b, expected_b,1e-16) + expected_a = input_to_split[0 : self.nn, :] + expected_b = input_to_split[self.nn : 2 * self.nn, :] + assert_near_equal(out_a, expected_a, 1e-16) + assert_near_equal(out_b, expected_b, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestSplitInitMethod(unittest.TestCase): +class TestSplitInitMethod(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='input_to_split', shape=(self.nn*2,3)) + ivc.add_output(name="input_to_split", shape=(self.nn * 2, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['input_to_split']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["input_to_split"]) - splitter=self.p.model.add_subsystem(name='vector_split_comp', - subsys=VectorSplitComp(['output_a','output_b'],'input_to_split',vec_sizes=[self.nn,self.nn],length=3)) - self.p.model.connect('input_to_split', 'vector_split_comp.input_to_split') + splitter = self.p.model.add_subsystem( + name="vector_split_comp", + subsys=VectorSplitComp(["output_a", "output_b"], "input_to_split", vec_sizes=[self.nn, self.nn], length=3), + ) + self.p.model.connect("input_to_split", "vector_split_comp.input_to_split") self.p.setup(force_alloc_complex=True) - self.p['input_to_split'] = np.random.rand(self.nn*2,3) + self.p["input_to_split"] = np.random.rand(self.nn * 2, 3) self.p.run_model() def test_results(self): - input_to_split = self.p['input_to_split'] - out_a = self.p['vector_split_comp.output_a'] - out_b = self.p['vector_split_comp.output_b'] + input_to_split = self.p["input_to_split"] + out_a = self.p["vector_split_comp.output_a"] + out_b = self.p["vector_split_comp.output_b"] - expected_a = input_to_split[0:self.nn,:] - expected_b = input_to_split[self.nn:2*self.nn,:] - assert_near_equal(out_a, expected_a,1e-16) - assert_near_equal(out_b, expected_b,1e-16) + expected_a = input_to_split[0 : self.nn, :] + expected_b = input_to_split[self.nn : 2 * self.nn, :] + assert_near_equal(out_a, expected_a, 1e-16) + assert_near_equal(out_b, expected_b, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestSplitMultipleSystems(unittest.TestCase): +class TestSplitMultipleSystems(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='input_to_split', shape=(self.nn*2+2,3)) + ivc.add_output(name="input_to_split", shape=(self.nn * 2 + 2, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['input_to_split']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["input_to_split"]) - splitter=self.p.model.add_subsystem(name='vector_split_comp', - subsys=VectorSplitComp()) - splitter.add_relation(['output_a','output_b','output_c'],'input_to_split',vec_sizes=[self.nn,self.nn,2],length=3) + splitter = self.p.model.add_subsystem(name="vector_split_comp", subsys=VectorSplitComp()) + splitter.add_relation( + ["output_a", "output_b", "output_c"], "input_to_split", vec_sizes=[self.nn, self.nn, 2], length=3 + ) - self.p.model.connect('input_to_split', 'vector_split_comp.input_to_split') + self.p.model.connect("input_to_split", "vector_split_comp.input_to_split") self.p.setup(force_alloc_complex=True) - self.p['input_to_split'] = np.random.rand(self.nn*2+2,3) + self.p["input_to_split"] = np.random.rand(self.nn * 2 + 2, 3) self.p.run_model() def test_results(self): - input_to_split = self.p['input_to_split'] - out_a = self.p['vector_split_comp.output_a'] - out_b = self.p['vector_split_comp.output_b'] - out_c = self.p['vector_split_comp.output_c'] + input_to_split = self.p["input_to_split"] + out_a = self.p["vector_split_comp.output_a"] + out_b = self.p["vector_split_comp.output_b"] + out_c = self.p["vector_split_comp.output_c"] - expected_a = input_to_split[0:self.nn,:] - expected_b = input_to_split[self.nn:2*self.nn,:] - expected_c = input_to_split[2*self.nn:2*self.nn+2,:] + expected_a = input_to_split[0 : self.nn, :] + expected_b = input_to_split[self.nn : 2 * self.nn, :] + expected_c = input_to_split[2 * self.nn : 2 * self.nn + 2, :] - assert_near_equal(out_a, expected_a,1e-16) - assert_near_equal(out_b, expected_b,1e-16) - assert_near_equal(out_c, expected_c,1e-16) + assert_near_equal(out_a, expected_a, 1e-16) + assert_near_equal(out_b, expected_b, 1e-16) + assert_near_equal(out_c, expected_c, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestSplitNx3Units(unittest.TestCase): +class TestSplitNx3Units(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='input_to_split', shape=(self.nn*2,3), units='m') + ivc.add_output(name="input_to_split", shape=(self.nn * 2, 3), units="m") - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['input_to_split']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["input_to_split"]) - splitter=self.p.model.add_subsystem(name='vector_split_comp', - subsys=VectorSplitComp()) - splitter.add_relation(['output_a','output_b'],'input_to_split',vec_sizes=[self.nn,self.nn],length=3, units='m') + splitter = self.p.model.add_subsystem(name="vector_split_comp", subsys=VectorSplitComp()) + splitter.add_relation( + ["output_a", "output_b"], "input_to_split", vec_sizes=[self.nn, self.nn], length=3, units="m" + ) - self.p.model.connect('input_to_split', 'vector_split_comp.input_to_split') + self.p.model.connect("input_to_split", "vector_split_comp.input_to_split") self.p.setup(force_alloc_complex=True) - self.p['input_to_split'] = np.random.rand(self.nn*2,3) + self.p["input_to_split"] = np.random.rand(self.nn * 2, 3) self.p.run_model() def test_results(self): - input_to_split = self.p['input_to_split'] - out_a = self.p['vector_split_comp.output_a'] - out_b = self.p['vector_split_comp.output_b'] + input_to_split = self.p["input_to_split"] + out_a = self.p["vector_split_comp.output_a"] + out_b = self.p["vector_split_comp.output_b"] - expected_a = input_to_split[0:self.nn,:] - expected_b = input_to_split[self.nn:2*self.nn,:] - assert_near_equal(out_a, expected_a,1e-16) - assert_near_equal(out_b, expected_b,1e-16) + expected_a = input_to_split[0 : self.nn, :] + expected_b = input_to_split[self.nn : 2 * self.nn, :] + assert_near_equal(out_a, expected_a, 1e-16) + assert_near_equal(out_b, expected_b, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestSplitWrongVecSizesOutputMismatch(unittest.TestCase): +class TestSplitWrongVecSizesOutputMismatch(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='input_to_split', shape=(self.nn*2,3)) + ivc.add_output(name="input_to_split", shape=(self.nn * 2, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['input_to_split']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["input_to_split"]) - splitter=self.p.model.add_subsystem(name='vector_split_comp', - subsys=VectorSplitComp()) - splitter.add_relation(['output_a','output_b'],'input_to_split',vec_sizes=[self.nn],length=3) + splitter = self.p.model.add_subsystem(name="vector_split_comp", subsys=VectorSplitComp()) + splitter.add_relation(["output_a", "output_b"], "input_to_split", vec_sizes=[self.nn], length=3) - self.p.model.connect('input_to_split', 'vector_split_comp.input_to_split') + self.p.model.connect("input_to_split", "vector_split_comp.input_to_split") def test_for_exception(self): - self.assertRaises(ValueError,self.p.setup) + self.assertRaises(ValueError, self.p.setup) -class TestForDocs(unittest.TestCase): +class TestForDocs(unittest.TestCase): def test(self): """ A simple example to illustrate usage of the concatenate/split components. @@ -614,47 +610,40 @@ def test(self): p = Problem(model=Group()) takeoff_conditions = IndepVarComp() - takeoff_conditions.add_output(name='velocity', shape=(n_takeoff_pts,2), units='m/s') - takeoff_conditions.add_output(name='altitude', shape=(n_takeoff_pts,), units='m') + takeoff_conditions.add_output(name="velocity", shape=(n_takeoff_pts, 2), units="m/s") + takeoff_conditions.add_output(name="altitude", shape=(n_takeoff_pts,), units="m") cruise_conditions = IndepVarComp() - cruise_conditions.add_output(name='velocity', shape=(n_cruise_pts,2), units='m/s') - cruise_conditions.add_output(name='altitude', shape=(n_cruise_pts,), units='m') + cruise_conditions.add_output(name="velocity", shape=(n_cruise_pts, 2), units="m/s") + cruise_conditions.add_output(name="altitude", shape=(n_cruise_pts,), units="m") - p.model.add_subsystem(name='takeoff_conditions', - subsys=takeoff_conditions) - p.model.add_subsystem(name='cruise_conditions', - subsys=cruise_conditions) + p.model.add_subsystem(name="takeoff_conditions", subsys=takeoff_conditions) + p.model.add_subsystem(name="cruise_conditions", subsys=cruise_conditions) - combiner=p.model.add_subsystem(name='combiner', - subsys=VectorConcatenateComp()) + combiner = p.model.add_subsystem(name="combiner", subsys=VectorConcatenateComp()) - combiner.add_relation('velocity',['to_vel','cruise_vel'], - vec_sizes=[3,5],length=2, units='m/s') + combiner.add_relation("velocity", ["to_vel", "cruise_vel"], vec_sizes=[3, 5], length=2, units="m/s") - combiner.add_relation('altitude',['to_alt','cruise_alt'], - vec_sizes=[3,5], units='m') + combiner.add_relation("altitude", ["to_alt", "cruise_alt"], vec_sizes=[3, 5], units="m") - p.model.connect('takeoff_conditions.velocity', 'combiner.to_vel') - p.model.connect('cruise_conditions.velocity', 'combiner.cruise_vel') - p.model.connect('takeoff_conditions.altitude', 'combiner.to_alt') - p.model.connect('cruise_conditions.altitude', 'combiner.cruise_alt') + p.model.connect("takeoff_conditions.velocity", "combiner.to_vel") + p.model.connect("cruise_conditions.velocity", "combiner.cruise_vel") + p.model.connect("takeoff_conditions.altitude", "combiner.to_alt") + p.model.connect("cruise_conditions.altitude", "combiner.cruise_alt") - divider=p.model.add_subsystem(name='divider', - subsys=VectorSplitComp()) - divider.add_relation(['to_vel','cruise_vel'],'velocity', - vec_sizes=[3,5],length=2, units='m/s') - p.model.connect('combiner.velocity','divider.velocity') + divider = p.model.add_subsystem(name="divider", subsys=VectorSplitComp()) + divider.add_relation(["to_vel", "cruise_vel"], "velocity", vec_sizes=[3, 5], length=2, units="m/s") + p.model.connect("combiner.velocity", "divider.velocity") p.setup(force_alloc_complex=True) - #set thrust to exceed drag, weight to equal lift for this scenario - p['takeoff_conditions.velocity'][:,0] = [30, 40, 50] - p['takeoff_conditions.velocity'][:,1] = [0, 0, 0] - p['cruise_conditions.velocity'][:,0] = [60, 60, 60, 60, 60] - p['cruise_conditions.velocity'][:,1] = [5, 0, 0, 0, -5] - p['takeoff_conditions.altitude'][:] = [0, 0, 0] - p['cruise_conditions.altitude'][:] = [6000,7500,8000,8500,5000] + # set thrust to exceed drag, weight to equal lift for this scenario + p["takeoff_conditions.velocity"][:, 0] = [30, 40, 50] + p["takeoff_conditions.velocity"][:, 1] = [0, 0, 0] + p["cruise_conditions.velocity"][:, 0] = [60, 60, 60, 60, 60] + p["cruise_conditions.velocity"][:, 1] = [5, 0, 0, 0, -5] + p["takeoff_conditions.altitude"][:] = [0, 0, 0] + p["cruise_conditions.altitude"][:] = [6000, 7500, 8000, 8500, 5000] p.run_model() @@ -662,9 +651,10 @@ def test(self): expected_vel = np.array([[30, 0], [40, 0], [50, 0], [60, 5], [60, 0], [60, 0], [60, 0], [60, -5]]) expected_alt = np.array([0, 0, 0, 6000, 7500, 8000, 8500, 5000]) expected_split_vel = np.array([[60, 5], [60, 0], [60, 0], [60, 0], [60, -5]]) - assert_near_equal(p.get_val('combiner.velocity', units='m/s'), expected_vel) - assert_near_equal(p.get_val('combiner.altitude', units='m'), expected_alt) - assert_near_equal(p.get_val('divider.cruise_vel', units='m/s'), expected_split_vel) + assert_near_equal(p.get_val("combiner.velocity", units="m/s"), expected_vel) + assert_near_equal(p.get_val("combiner.altitude", units="m"), expected_alt) + assert_near_equal(p.get_val("divider.cruise_vel", units="m/s"), expected_split_vel) + -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/openconcept/utilities/math/tests/test_integrals.py b/openconcept/utilities/math/tests/test_integrals.py index 8977eae0..28748bd2 100644 --- a/openconcept/utilities/math/tests/test_integrals.py +++ b/openconcept/utilities/math/tests/test_integrals.py @@ -5,6 +5,7 @@ from openmdao.api import IndepVarComp, Group, Problem from openconcept.utilities import Integrator + class IntegratorTestGroup(Group): """An OpenMDAO group to test the every-node integrator component @@ -21,90 +22,99 @@ class IntegratorTestGroup(Group): """ def initialize(self): - self.options.declare('quantity_units',default=None, desc="Units of the quantity being differentiated") - self.options.declare('diff_units',default=None, desc="Units of the differential") - self.options.declare('rate_units',default=None, desc="Units of the rate") - self.options.declare('num_nodes',default=11, desc="Number of nodes per segment") - self.options.declare('integrator',default='simpson', desc="Which simpson integrator to use") - self.options.declare('time_setup',default='dt') - self.options.declare('second_integrand',default=False) - self.options.declare('zero_start', default=False) - self.options.declare('final_only', default=False) - self.options.declare('test_auto_names', default=False) - self.options.declare('val', default=0.0) + self.options.declare("quantity_units", default=None, desc="Units of the quantity being differentiated") + self.options.declare("diff_units", default=None, desc="Units of the differential") + self.options.declare("rate_units", default=None, desc="Units of the rate") + self.options.declare("num_nodes", default=11, desc="Number of nodes per segment") + self.options.declare("integrator", default="simpson", desc="Which simpson integrator to use") + self.options.declare("time_setup", default="dt") + self.options.declare("second_integrand", default=False) + self.options.declare("zero_start", default=False) + self.options.declare("final_only", default=False) + self.options.declare("test_auto_names", default=False) + self.options.declare("val", default=0.0) def setup(self): - quantity_units = self.options['quantity_units'] - diff_units = self.options['diff_units'] - rate_units = self.options['rate_units'] - num_nodes = self.options['num_nodes'] - integrator_option = self.options['integrator'] - time_setup = self.options['time_setup'] - second_integrand = self.options['second_integrand'] - zero_start = self.options['zero_start'] - final_only = self.options['final_only'] - test_auto_names = self.options['test_auto_names'] - val = self.options['val'] + quantity_units = self.options["quantity_units"] + diff_units = self.options["diff_units"] + rate_units = self.options["rate_units"] + num_nodes = self.options["num_nodes"] + integrator_option = self.options["integrator"] + time_setup = self.options["time_setup"] + second_integrand = self.options["second_integrand"] + zero_start = self.options["zero_start"] + final_only = self.options["final_only"] + test_auto_names = self.options["test_auto_names"] + val = self.options["val"] num_nodes = num_nodes - iv = self.add_subsystem('iv', IndepVarComp()) - integ = Integrator(diff_units=diff_units, num_nodes=num_nodes, method=integrator_option, - time_setup=time_setup) + iv = self.add_subsystem("iv", IndepVarComp()) + integ = Integrator(diff_units=diff_units, num_nodes=num_nodes, method=integrator_option, time_setup=time_setup) if not test_auto_names: - integ.add_integrand('q', rate_name='dqdt', start_name='q_initial', end_name='q_final', - units=quantity_units, rate_units=rate_units, zero_start=zero_start, - final_only=final_only, val=val) + integ.add_integrand( + "q", + rate_name="dqdt", + start_name="q_initial", + end_name="q_final", + units=quantity_units, + rate_units=rate_units, + zero_start=zero_start, + final_only=final_only, + val=val, + ) else: - integ.add_integrand('q', units=quantity_units, rate_units=rate_units, zero_start=zero_start, - final_only=final_only) + integ.add_integrand( + "q", units=quantity_units, rate_units=rate_units, zero_start=zero_start, final_only=final_only + ) if second_integrand: - integ.add_integrand('q2', rate_name='dq2dt', start_name='q2_initial', end_name='q2_final', units='kJ') - iv.add_output('rate_to_integrate_2', val=np.ones((num_nodes,)), units='kW') - iv.add_output('initial_value_2', val=0., units='kJ') - self.connect('iv.rate_to_integrate_2', 'integral.dq2dt') - self.connect('iv.initial_value_2', 'integral.q2_initial') - self.add_subsystem('integral', integ) + integ.add_integrand("q2", rate_name="dq2dt", start_name="q2_initial", end_name="q2_final", units="kJ") + iv.add_output("rate_to_integrate_2", val=np.ones((num_nodes,)), units="kW") + iv.add_output("initial_value_2", val=0.0, units="kJ") + self.connect("iv.rate_to_integrate_2", "integral.dq2dt") + self.connect("iv.initial_value_2", "integral.q2_initial") + self.add_subsystem("integral", integ) if rate_units and quantity_units: # overdetermined and possibly inconsistent pass elif not rate_units and not quantity_units: if diff_units: - rate_units = '(' + diff_units +')** -1' + rate_units = "(" + diff_units + ")** -1" elif not rate_units: # solve for rate_units in terms of quantity_units if not diff_units: rate_units = quantity_units else: - rate_units = '('+quantity_units+') / (' + diff_units +')' + rate_units = "(" + quantity_units + ") / (" + diff_units + ")" elif not quantity_units: # solve for quantity units in terms of rate units if not diff_units: quantity_units = rate_units else: - quantity_units = '('+rate_units+')*('+diff_units+')' + quantity_units = "(" + rate_units + ")*(" + diff_units + ")" - iv.add_output('rate_to_integrate', val=np.ones((num_nodes,)), units=rate_units) - iv.add_output('initial_value', val=0, units=quantity_units) + iv.add_output("rate_to_integrate", val=np.ones((num_nodes,)), units=rate_units) + iv.add_output("initial_value", val=0, units=quantity_units) if not test_auto_names: - self.connect('iv.rate_to_integrate','integral.dqdt') + self.connect("iv.rate_to_integrate", "integral.dqdt") else: - self.connect('iv.rate_to_integrate','integral.q_rate') + self.connect("iv.rate_to_integrate", "integral.q_rate") if not zero_start: - self.connect('iv.initial_value', 'integral.q_initial') - - if time_setup == 'dt': - iv.add_output('dt', val=1, units=diff_units) - self.connect('iv.dt', 'integral.dt') - elif time_setup == 'duration': - iv.add_output('duration', val=1*(num_nodes-1), units=diff_units) - self.connect('iv.duration', 'integral.duration') - elif time_setup == 'bounds': - iv.add_output('t_initial', val=2, units=diff_units) - iv.add_output('t_final', val=2 + 1*(num_nodes-1), units=diff_units) - self.connect('iv.t_initial','integral.t_initial') - self.connect('iv.t_final','integral.t_final') + self.connect("iv.initial_value", "integral.q_initial") + + if time_setup == "dt": + iv.add_output("dt", val=1, units=diff_units) + self.connect("iv.dt", "integral.dt") + elif time_setup == "duration": + iv.add_output("duration", val=1 * (num_nodes - 1), units=diff_units) + self.connect("iv.duration", "integral.duration") + elif time_setup == "bounds": + iv.add_output("t_initial", val=2, units=diff_units) + iv.add_output("t_final", val=2 + 1 * (num_nodes - 1), units=diff_units) + self.connect("iv.t_initial", "integral.t_initial") + self.connect("iv.t_final", "integral.t_final") + class IntegratorCommonTestCases(object): """ @@ -117,292 +127,349 @@ def test_uniform_no_units(self): prob.run_model() num_nodes = self.num_nodes nn_tot = num_nodes - assert_near_equal(prob['integral.q'], np.linspace(0, nn_tot-1, nn_tot), tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), nn_tot-1, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["integral.q"], np.linspace(0, nn_tot - 1, nn_tot), tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), nn_tot - 1, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_linear_no_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) + x = np.linspace(0, nn_tot - 1, nn_tot) fprime = x - f = x ** 2 / 2 + f = x**2 / 2 prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator)) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob['integral.q'], f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["integral.q"], f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_no_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator)) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob['integral.q'], f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["integral.q"], f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_machine_zero_rate(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) + x = np.linspace(0, nn_tot - 1, nn_tot) fprime = 0.0 * x f = 0.0 * x prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator)) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob['integral.q'], f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["integral.q"], f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_auto_names(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x prob = Problem(IntegratorTestGroup(test_auto_names=True, num_nodes=self.num_nodes, integrator=self.integrator)) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob['integral.q'], f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["integral.q"], f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_qty_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s')) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, integrator=self.integrator, quantity_units="kg", diff_units="s" + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_qty_units_nonzero_start(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + 25.2 - - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s')) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + 25.2 + + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, integrator=self.integrator, quantity_units="kg", diff_units="s" + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime - prob['iv.initial_value'] = 25.2 + prob["iv.rate_to_integrate"] = fprime + prob["iv.initial_value"] = 25.2 prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_qty_units_zero_start(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s', - zero_start=True)) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + zero_start=True, + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) with self.assertRaises(KeyError): # ensure the input hasn't been created - prob['integral.q_initial'] = -1.0 + prob["integral.q_initial"] = -1.0 def test_quadratic_qty_units_final_only(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s', - final_only=True)) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + final_only=True, + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() with self.assertRaises(KeyError): # this output shouldn't exist - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_rate_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - rate_units='kg/s', diff_units='s')) + prob = Problem( + IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, rate_units="kg/s", diff_units="s") + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_multiple_integrands(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime1 = 4 * x **2 - 8*x + 5 - f1 = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - fprime2 = -2 * x **2 + 10.5*x - 2 - f2 = -2 * x ** 3 / 3 + 10.5*x**2 /2 - 2*x - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - rate_units='kg/s', diff_units='s', second_integrand=True)) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime1 = 4 * x**2 - 8 * x + 5 + f1 = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + fprime2 = -2 * x**2 + 10.5 * x - 2 + f2 = -2 * x**3 / 3 + 10.5 * x**2 / 2 - 2 * x + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + rate_units="kg/s", + diff_units="s", + second_integrand=True, + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime1 - prob['iv.rate_to_integrate_2'] = fprime2 + prob["iv.rate_to_integrate"] = fprime1 + prob["iv.rate_to_integrate_2"] = fprime2 prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f1, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f1[-1], tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q2', units='kJ'), f2, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q2_final', units='kJ'), f2[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f1, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f1[-1], tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q2", units="kJ"), f2, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q2_final", units="kJ"), f2[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) - def test_quadratic_both_units_correct(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - rate_units='kg/s', quantity_units='kg', diff_units='s')) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + rate_units="kg/s", + quantity_units="kg", + diff_units="s", + ) + ) with self.assertRaises(ValueError) as cm: prob.setup(check=True) - - msg = ('Specify either quantity units or rate units, but not both') + + msg = "Specify either quantity units or rate units, but not both" self.assertEqual(str(cm.exception), msg) def test_quadratic_duration(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s',time_setup='duration')) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + time_setup="duration", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_bounds(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s',time_setup='bounds')) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + time_setup="bounds", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_machine_zero_bounds(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) + x = np.linspace(0, nn_tot - 1, nn_tot) fprime = 0.0 * x - f = 0.0*x + f = 0.0 * x - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s',time_setup='bounds')) + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + time_setup="bounds", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_no_rate_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - diff_units='s')) + prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, diff_units="s")) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units=None), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units=None), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) + class SimpsonIntegrator5PtTestCases(unittest.TestCase, IntegratorCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 11 - self.integrator = 'simpson' + self.integrator = "simpson" super(SimpsonIntegrator5PtTestCases, self).__init__(*args, **kwargs) + class SimpsonIntegrator3PtTestCases(unittest.TestCase, IntegratorCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 7 - self.integrator = 'simpson' + self.integrator = "simpson" super(SimpsonIntegrator3PtTestCases, self).__init__(*args, **kwargs) + @pytest.mark.filterwarnings("ignore:divide by zero") @pytest.mark.filterwarnings("ignore:invalid value") class SimpsonIntegrator1PtTestCases(unittest.TestCase, IntegratorCommonTestCases): @@ -410,78 +477,101 @@ class SimpsonIntegrator1PtTestCases(unittest.TestCase, IntegratorCommonTestCases Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 1 - self.integrator = 'simpson' + self.integrator = "simpson" super(SimpsonIntegrator1PtTestCases, self).__init__(*args, **kwargs) + class BDFIntegrator5PtTestCases(unittest.TestCase, IntegratorCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 11 - self.integrator = 'bdf3' + self.integrator = "bdf3" super(BDFIntegrator5PtTestCases, self).__init__(*args, **kwargs) + class BDFIntegrator3PtTestCases(unittest.TestCase, IntegratorCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 7 - self.integrator = 'bdf3' + self.integrator = "bdf3" super(BDFIntegrator3PtTestCases, self).__init__(*args, **kwargs) + class EdgeCaseTestCases(unittest.TestCase): def test_quadratic_even_num_nodes(self): num_nodes = 10 nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x with self.assertRaises(ValueError) as cm: - prob = Problem(IntegratorTestGroup(num_nodes=num_nodes, integrator='simpson', - rate_units='kg/s', diff_units='s')) + prob = Problem( + IntegratorTestGroup(num_nodes=num_nodes, integrator="simpson", rate_units="kg/s", diff_units="s") + ) prob.setup() - - msg = ('num_nodes is ' +str(num_nodes) + ' and must be odd') + + msg = "num_nodes is " + str(num_nodes) + " and must be odd" self.assertEqual(str(cm.exception), msg) def test_default_value_scalar(self): num_nodes = self.num_nodes = 11 nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator='simpson', - quantity_units='kg', diff_units='s',time_setup='duration', - val=5.0)) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator="simpson", + quantity_units="kg", + diff_units="s", + time_setup="duration", + val=5.0, + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime # DO NOT RUN THE MODEL - assert_near_equal(prob.get_val('integral.q', units='kg'), 5.0*np.ones((num_nodes,)), tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), 5.0, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), 5.0 * np.ones((num_nodes,)), tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), 5.0, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_default_value_vector(self): num_nodes = self.num_nodes = 11 nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(IntegratorTestGroup(num_nodes=self.num_nodes, integrator='simpson', - quantity_units='kg', diff_units='s',time_setup='duration', - val=5.0*np.linspace(0.0, 1.0, num_nodes))) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + IntegratorTestGroup( + num_nodes=self.num_nodes, + integrator="simpson", + quantity_units="kg", + diff_units="s", + time_setup="duration", + val=5.0 * np.linspace(0.0, 1.0, num_nodes), + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime # DO NOT RUN THE MODEL - assert_near_equal(prob.get_val('integral.q', units='kg'), 5.0*np.linspace(0.0, 1.0, num_nodes), tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), 5.0, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal( + prob.get_val("integral.q", units="kg"), 5.0 * np.linspace(0.0, 1.0, num_nodes), tolerance=1e-14 + ) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), 5.0, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) diff --git a/openconcept/utilities/math/tests/test_math.py b/openconcept/utilities/math/tests/test_math.py index aa1d3a52..3626600b 100644 --- a/openconcept/utilities/math/tests/test_math.py +++ b/openconcept/utilities/math/tests/test_math.py @@ -5,6 +5,7 @@ from openmdao.api import IndepVarComp, Group, Problem from openconcept.utilities import FirstDerivative + class FirstDerivativeTestGroup(Group): """An OpenMDAO group to test the differentiation tools @@ -26,35 +27,44 @@ class FirstDerivativeTestGroup(Group): """ def initialize(self): - self.options.declare('segment_names', default=None, desc="Names of differentiation segments") - self.options.declare('quantity_units',default=None, desc="Units of the quantity being differentiated") - self.options.declare('diff_units',default=None, desc="Units of the differential") - self.options.declare('num_intervals',default=5, desc="Number of Simpsons rule intervals per segment") - self.options.declare('order',default=4, desc="Order of accuracy") + self.options.declare("segment_names", default=None, desc="Names of differentiation segments") + self.options.declare("quantity_units", default=None, desc="Units of the quantity being differentiated") + self.options.declare("diff_units", default=None, desc="Units of the differential") + self.options.declare("num_intervals", default=5, desc="Number of Simpsons rule intervals per segment") + self.options.declare("order", default=4, desc="Order of accuracy") def setup(self): - segment_names = self.options['segment_names'] - quantity_units = self.options['quantity_units'] - diff_units = self.options['diff_units'] - order = self.options['order'] - n_int_per_seg = self.options['num_intervals'] + segment_names = self.options["segment_names"] + quantity_units = self.options["quantity_units"] + diff_units = self.options["diff_units"] + order = self.options["order"] + n_int_per_seg = self.options["num_intervals"] if segment_names is None: - nn_tot = (2*n_int_per_seg + 1) + nn_tot = 2 * n_int_per_seg + 1 else: - nn_tot = (2*n_int_per_seg + 1) * len(segment_names) - iv = self.add_subsystem('iv', IndepVarComp()) - self.add_subsystem('derivative', FirstDerivative(segment_names=segment_names, quantity_units=quantity_units, - diff_units=diff_units, num_intervals=n_int_per_seg, order=order)) - iv.add_output('quant_to_diff', val=np.ones((nn_tot,)), units=quantity_units) - self.connect('iv.quant_to_diff','derivative.q') + nn_tot = (2 * n_int_per_seg + 1) * len(segment_names) + iv = self.add_subsystem("iv", IndepVarComp()) + self.add_subsystem( + "derivative", + FirstDerivative( + segment_names=segment_names, + quantity_units=quantity_units, + diff_units=diff_units, + num_intervals=n_int_per_seg, + order=order, + ), + ) + iv.add_output("quant_to_diff", val=np.ones((nn_tot,)), units=quantity_units) + self.connect("iv.quant_to_diff", "derivative.q") if segment_names is None: - iv.add_output('dt', val=1, units=diff_units) - self.connect('iv.dt', 'derivative.dt') + iv.add_output("dt", val=1, units=diff_units) + self.connect("iv.dt", "derivative.dt") else: for segment_name in segment_names: - iv.add_output(segment_name + '|dt', val=1, units=diff_units) - self.connect('iv.'+segment_name + '|dt','derivative.'+segment_name + '|dt') + iv.add_output(segment_name + "|dt", val=1, units=diff_units) + self.connect("iv." + segment_name + "|dt", "derivative." + segment_name + "|dt") + class FirstDerivCommonTestCases(object): """ @@ -66,134 +76,150 @@ def test_uniform_single_phase_no_units(self): prob.setup(check=True, force_alloc_complex=True) prob.run_model() n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) - assert_near_equal(prob['derivative.dqdt'], np.zeros((nn_tot,)),tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + nn_tot = n_int_per_seg * 2 + 1 + assert_near_equal(prob["derivative.dqdt"], np.zeros((nn_tot,)), tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e2) def test_linear_single_phase_no_units(self): prob = Problem(FirstDerivativeTestGroup(order=self.order)) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) - prob['iv.quant_to_diff'] = np.linspace(0,1,nn_tot) - prob['iv.dt'] = 1 / (nn_tot - 1) + nn_tot = n_int_per_seg * 2 + 1 + prob["iv.quant_to_diff"] = np.linspace(0, 1, nn_tot) + prob["iv.dt"] = 1 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob['derivative.dqdt'], np.ones((nn_tot,)),tolerance=2e-15) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["derivative.dqdt"], np.ones((nn_tot,)), tolerance=2e-15) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e2) def test_quadratic_single_phase_no_units(self): prob = Problem(FirstDerivativeTestGroup(order=self.order)) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(0, 2, nn_tot) - f_test = 5*x ** 2+ 7*x -3 - fp_exact = 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.dt'] = 2 / (nn_tot - 1) + f_test = 5 * x**2 + 7 * x - 3 + fp_exact = 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob['derivative.dqdt'], fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["derivative.dqdt"], fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e-8) def test_quadratic_single_phase_units(self): - prob = Problem(FirstDerivativeTestGroup(order=self.order, quantity_units='m', diff_units='s')) + prob = Problem(FirstDerivativeTestGroup(order=self.order, quantity_units="m", diff_units="s")) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(0, 2, nn_tot) - f_test = 5*x ** 2+ 7*x -3 - fp_exact = 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.dt'] = 2 / (nn_tot - 1) + f_test = 5 * x**2 + 7 * x - 3 + fp_exact = 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob.get_val('derivative.dqdt','m/s'), fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("derivative.dqdt", "m/s"), fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e-8) def test_quadratic_single_phase_diff_units_only(self): - prob = Problem(FirstDerivativeTestGroup(order=self.order, diff_units='s')) + prob = Problem(FirstDerivativeTestGroup(order=self.order, diff_units="s")) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(0, 2, nn_tot) - f_test = 5*x ** 2+ 7*x -3 - fp_exact = 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.dt'] = 2 / (nn_tot - 1) + f_test = 5 * x**2 + 7 * x - 3 + fp_exact = 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob.get_val('derivative.dqdt','s ** -1'), fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("derivative.dqdt", "s ** -1"), fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e-8) def test_quadratic_single_named_phase_units(self): - prob = Problem(FirstDerivativeTestGroup(segment_names=['cruise'], order=self.order, quantity_units='m', diff_units='s')) + prob = Problem( + FirstDerivativeTestGroup(segment_names=["cruise"], order=self.order, quantity_units="m", diff_units="s") + ) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(0, 2, nn_tot) - f_test = 5*x ** 2+ 7*x -3 - fp_exact = 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.cruise|dt'] = 2 / (nn_tot - 1) + f_test = 5 * x**2 + 7 * x - 3 + fp_exact = 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.cruise|dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob.get_val('derivative.dqdt','m/s'), fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("derivative.dqdt", "m/s"), fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e-8) def test_quadratic_multi_phase_units(self): - prob = Problem(FirstDerivativeTestGroup(segment_names=['climb','cruise','descent'], - order=self.order, quantity_units='m', diff_units='s')) + prob = Problem( + FirstDerivativeTestGroup( + segment_names=["climb", "cruise", "descent"], order=self.order, quantity_units="m", diff_units="s" + ) + ) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_seg = (n_int_per_seg*2 + 1) - nn_tot = (n_int_per_seg*2 + 1) * 3 + nn_seg = n_int_per_seg * 2 + 1 + nn_tot = (n_int_per_seg * 2 + 1) * 3 x = np.concatenate([np.linspace(0, 2, nn_seg), np.linspace(2, 3, nn_seg), np.linspace(3, 6, nn_seg)]) - f_test = 5*x ** 2+ 7*x -3 - fp_exact = 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.climb|dt'] = 2 / (nn_seg - 1) - prob['iv.cruise|dt'] = 1 / (nn_seg - 1) - prob['iv.descent|dt'] = 3 / (nn_seg - 1) + f_test = 5 * x**2 + 7 * x - 3 + fp_exact = 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.climb|dt"] = 2 / (nn_seg - 1) + prob["iv.cruise|dt"] = 1 / (nn_seg - 1) + prob["iv.descent|dt"] = 3 / (nn_seg - 1) prob.run_model() - assert_near_equal(prob.get_val('derivative.dqdt','m/s'), fp_exact, tolerance=1e-12) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("derivative.dqdt", "m/s"), fp_exact, tolerance=1e-12) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-6, rtol=1e-6) def test_quadratic_multi_phase_units_7int(self): - prob = Problem(FirstDerivativeTestGroup(segment_names=['climb','cruise','descent'], - order=self.order, quantity_units='m', diff_units='s', num_intervals=7)) + prob = Problem( + FirstDerivativeTestGroup( + segment_names=["climb", "cruise", "descent"], + order=self.order, + quantity_units="m", + diff_units="s", + num_intervals=7, + ) + ) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 7 - nn_seg = (n_int_per_seg*2 + 1) - nn_tot = (n_int_per_seg*2 + 1) * 3 + nn_seg = n_int_per_seg * 2 + 1 + nn_tot = (n_int_per_seg * 2 + 1) * 3 x = np.concatenate([np.linspace(0, 2, nn_seg), np.linspace(2, 3, nn_seg), np.linspace(3, 6, nn_seg)]) - f_test = 5*x ** 2+ 7*x -3 - fp_exact = 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.climb|dt'] = 2 / (nn_seg - 1) - prob['iv.cruise|dt'] = 1 / (nn_seg - 1) - prob['iv.descent|dt'] = 3 / (nn_seg - 1) + f_test = 5 * x**2 + 7 * x - 3 + fp_exact = 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.climb|dt"] = 2 / (nn_seg - 1) + prob["iv.cruise|dt"] = 1 / (nn_seg - 1) + prob["iv.descent|dt"] = 3 / (nn_seg - 1) prob.run_model() - assert_near_equal(prob.get_val('derivative.dqdt','m/s'), fp_exact, tolerance=1e-12) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("derivative.dqdt", "m/s"), fp_exact, tolerance=1e-12) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-6, rtol=1e-6) + class FirstDerivativeSecondOrderTestCases(unittest.TestCase, FirstDerivCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.order = 2 super(FirstDerivativeSecondOrderTestCases, self).__init__(*args, **kwargs) + class FirstDerivativeFourthOrderTestCases(unittest.TestCase, FirstDerivCommonTestCases): """ Add some additional fourth order polynomial test cases. """ + def __init__(self, *args, **kwargs): self.order = 4 super(FirstDerivativeFourthOrderTestCases, self).__init__(*args, **kwargs) @@ -202,75 +228,74 @@ def test_quartic_single_phase_no_units(self): prob = Problem(FirstDerivativeTestGroup(order=self.order)) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(0, 2, nn_tot) - f_test = x ** 4 - 3*x **3 + 5*x ** 2+ 7*x -3 - fp_exact = 4*x ** 3 - 9*x ** 2 + 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.dt'] = 2 / (nn_tot - 1) + f_test = x**4 - 3 * x**3 + 5 * x**2 + 7 * x - 3 + fp_exact = 4 * x**3 - 9 * x**2 + 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob['derivative.dqdt'], fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["derivative.dqdt"], fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e-8) def test_quartic_negative_single_phase_no_units(self): prob = Problem(FirstDerivativeTestGroup(order=self.order)) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(-3, -1, nn_tot) - f_test = x ** 4 - 3*x **3 + 5*x ** 2+ 7*x -3 - fp_exact = 4*x ** 3 - 9*x ** 2 + 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.dt'] = 2 / (nn_tot - 1) + f_test = x**4 - 3 * x**3 + 5 * x**2 + 7 * x - 3 + fp_exact = 4 * x**3 - 9 * x**2 + 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob['derivative.dqdt'], fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["derivative.dqdt"], fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-6, rtol=1e-6) def test_quartic_single_phase_units(self): - prob = Problem(FirstDerivativeTestGroup(order=self.order, quantity_units='m', diff_units='s')) + prob = Problem(FirstDerivativeTestGroup(order=self.order, quantity_units="m", diff_units="s")) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(0, 2, nn_tot) - f_test = x ** 4 - 3*x **3 + 5*x ** 2+ 7*x -3 - fp_exact = 4*x ** 3 - 9*x ** 2 + 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.dt'] = 2 / (nn_tot - 1) + f_test = x**4 - 3 * x**3 + 5 * x**2 + 7 * x - 3 + fp_exact = 4 * x**3 - 9 * x**2 + 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob.get_val('derivative.dqdt',units='m/s'), fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("derivative.dqdt", units="m/s"), fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e-8) def test_quartic_single_phase_diff_units_only(self): - prob = Problem(FirstDerivativeTestGroup(order=self.order, diff_units='s')) + prob = Problem(FirstDerivativeTestGroup(order=self.order, diff_units="s")) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(0, 2, nn_tot) - f_test = x ** 4 - 3*x **3 + 5*x ** 2+ 7*x -3 - fp_exact = 4*x ** 3 - 9*x ** 2 + 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.dt'] = 2 / (nn_tot - 1) + f_test = x**4 - 3 * x**3 + 5 * x**2 + 7 * x - 3 + fp_exact = 4 * x**3 - 9 * x**2 + 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob.get_val('derivative.dqdt',units='s ** -1'), fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("derivative.dqdt", units="s ** -1"), fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e-8) @pytest.mark.filterwarnings("ignore:You have specified*:UserWarning") def test_quartic_single_phase_qty_units_only(self): - prob = Problem(FirstDerivativeTestGroup(order=self.order, quantity_units='m')) + prob = Problem(FirstDerivativeTestGroup(order=self.order, quantity_units="m")) prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 - nn_tot = (n_int_per_seg*2 + 1) + nn_tot = n_int_per_seg * 2 + 1 x = np.linspace(0, 2, nn_tot) - f_test = x ** 4 - 3*x **3 + 5*x ** 2+ 7*x -3 - fp_exact = 4*x ** 3 - 9*x ** 2 + 10*x + 7 - prob['iv.quant_to_diff'] = f_test - prob['iv.dt'] = 2 / (nn_tot - 1) + f_test = x**4 - 3 * x**3 + 5 * x**2 + 7 * x - 3 + fp_exact = 4 * x**3 - 9 * x**2 + 10 * x + 7 + prob["iv.quant_to_diff"] = f_test + prob["iv.dt"] = 2 / (nn_tot - 1) prob.run_model() - assert_near_equal(prob.get_val('derivative.dqdt',units='m'), fp_exact, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("derivative.dqdt", units="m"), fp_exact, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e-8) - diff --git a/openconcept/utilities/math/tests/test_min_max_comp.py b/openconcept/utilities/math/tests/test_min_max_comp.py index d574b479..8641b7d0 100644 --- a/openconcept/utilities/math/tests/test_min_max_comp.py +++ b/openconcept/utilities/math/tests/test_min_max_comp.py @@ -4,129 +4,133 @@ from openmdao.api import Problem from openconcept.utilities import MaxComp, MinComp + class MaxCompTestCase(unittest.TestCase): """ Test the MaxComp component """ + def test_one_input(self): p = Problem() - p.model.add_subsystem('test', MaxComp(), promotes=['*']) + p.model.add_subsystem("test", MaxComp(), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([42.])) + p.set_val("array", np.array([42.0])) p.run_model() - assert_near_equal(p['max'], 42.) + assert_near_equal(p["max"], 42.0) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_multiple_inputs(self): nn = 5 p = Problem() - p.model.add_subsystem('test', MaxComp(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", MaxComp(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([42., 12., -3., 58., 7.])) + p.set_val("array", np.array([42.0, 12.0, -3.0, 58.0, 7.0])) p.run_model() - assert_near_equal(p['max'], 58.) + assert_near_equal(p["max"], 58.0) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_multiple_max(self): nn = 5 p = Problem() - p.model.add_subsystem('test', MaxComp(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", MaxComp(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([42., 58., -3., 58., 7.])) + p.set_val("array", np.array([42.0, 58.0, -3.0, 58.0, 7.0])) p.run_model() - assert_near_equal(p['max'], 58.) + assert_near_equal(p["max"], 58.0) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_multiple_very_close_max(self): nn = 5 p = Problem() - p.model.add_subsystem('test', MaxComp(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", MaxComp(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([-2., 2e-45, -3., 1e-45, -7.])) + p.set_val("array", np.array([-2.0, 2e-45, -3.0, 1e-45, -7.0])) p.run_model() - assert_near_equal(p['max'], 2e-45, tolerance=1e-50) + assert_near_equal(p["max"], 2e-45, tolerance=1e-50) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_units(self): nn = 5 p = Problem() - p.model.add_subsystem('max_comp', MaxComp(num_nodes=nn, units='N'), promotes=['*']) + p.model.add_subsystem("max_comp", MaxComp(num_nodes=nn, units="N"), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([42., 58., -3., 3., 7.]), units='N') + p.set_val("array", np.array([42.0, 58.0, -3.0, 3.0, 7.0]), units="N") p.run_model() - assert_near_equal(p['max'], 58.) + assert_near_equal(p["max"], 58.0) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) + class MinCompTestCase(unittest.TestCase): """ Test the MinComp component """ + def test_one_input(self): p = Problem() - p.model.add_subsystem('test', MinComp(), promotes=['*']) + p.model.add_subsystem("test", MinComp(), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([42.])) + p.set_val("array", np.array([42.0])) p.run_model() - assert_near_equal(p['min'], 42.) + assert_near_equal(p["min"], 42.0) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_multiple_inputs(self): nn = 5 p = Problem() - p.model.add_subsystem('test', MinComp(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", MinComp(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([42., 12., -3., 58., 7.])) + p.set_val("array", np.array([42.0, 12.0, -3.0, 58.0, 7.0])) p.run_model() - assert_near_equal(p['min'], -3.) + assert_near_equal(p["min"], -3.0) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_multiple_min(self): nn = 5 p = Problem(MinComp(num_nodes=nn)) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([42., 7., 30., 58., 7.])) + p.set_val("array", np.array([42.0, 7.0, 30.0, 58.0, 7.0])) p.run_model() - assert_near_equal(p['min'], 7.) + assert_near_equal(p["min"], 7.0) # Need to use fd here because cs doesn't support backward and forward does # not capture behavior of minimum - partials = p.check_partials(method='fd',form='backward',compact_print=True) + partials = p.check_partials(method="fd", form="backward", compact_print=True) assert_check_partials(partials) - + def test_multiple_very_close_min(self): nn = 5 p = Problem() - p.model.add_subsystem('test', MinComp(num_nodes=nn), promotes=['*']) + p.model.add_subsystem("test", MinComp(num_nodes=nn), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([2., 2e-45, 3., 1e-45, 7.])) + p.set_val("array", np.array([2.0, 2e-45, 3.0, 1e-45, 7.0])) p.run_model() - assert_near_equal(p['min'], 1e-45, tolerance=1e-50) + assert_near_equal(p["min"], 1e-45, tolerance=1e-50) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - + def test_units(self): nn = 5 p = Problem() - p.model.add_subsystem('min_comp', MinComp(num_nodes=nn, units='N'), promotes=['*']) + p.model.add_subsystem("min_comp", MinComp(num_nodes=nn, units="N"), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('array', np.array([42., 58., -3., 3., 7.]), units='N') + p.set_val("array", np.array([42.0, 58.0, -3.0, 3.0, 7.0]), units="N") p.run_model() - assert_near_equal(p['min'], -3.) + assert_near_equal(p["min"], -3.0) - partials = p.check_partials(method='cs',compact_print=True) - assert_check_partials(partials) \ No newline at end of file + partials = p.check_partials(method="cs", compact_print=True) + assert_check_partials(partials) diff --git a/openconcept/utilities/math/tests/test_multiply_divide_comp.py b/openconcept/utilities/math/tests/test_multiply_divide_comp.py index 90d62e5e..cb623fde 100644 --- a/openconcept/utilities/math/tests/test_multiply_divide_comp.py +++ b/openconcept/utilities/math/tests/test_multiply_divide_comp.py @@ -8,443 +8,438 @@ from openconcept.utilities import ElementMultiplyDivideComp from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -class TestElementMultiplyDivideCompScalars(unittest.TestCase): +class TestElementMultiplyDivideCompScalars(unittest.TestCase): def setUp(self): self.nn = 1 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b']) + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation("multdiv_output", ["input_a", "input_b"]) - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") self.p.setup() - self.p['a'] = np.random.rand(self.nn,) - self.p['b'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) + self.p["b"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['multiply_divide_comp.multdiv_output'] + a = self.p["a"] + b = self.p["b"] + out = self.p["multiply_divide_comp.multdiv_output"] expected = a * b - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestElementMultiplyDivideCompNx1(unittest.TestCase): +class TestElementMultiplyDivideCompNx1(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b'],vec_size=self.nn) + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation("multdiv_output", ["input_a", "input_b"], vec_size=self.nn) - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") self.p.setup() - self.p['a'] = np.random.rand(self.nn,) - self.p['b'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) + self.p["b"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['multiply_divide_comp.multdiv_output'] + a = self.p["a"] + b = self.p["b"] + out = self.p["multiply_divide_comp.multdiv_output"] expected = a * b - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestElementMultiplyVectorScalar(unittest.TestCase): +class TestElementMultiplyVectorScalar(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', val=3.0) + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", val=3.0) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b'],vec_size=[self.nn, 1]) + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation("multdiv_output", ["input_a", "input_b"], vec_size=[self.nn, 1]) - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") self.p.setup() - self.p['a'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['multiply_divide_comp.multdiv_output'] + a = self.p["a"] + b = self.p["b"] + out = self.p["multiply_divide_comp.multdiv_output"] expected = a * b - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestElementMultiplyDivideCompNx3(unittest.TestCase): +class TestElementMultiplyDivideCompNx3(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b'],vec_size=self.nn,length=3) + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation("multdiv_output", ["input_a", "input_b"], vec_size=self.nn, length=3) - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") self.p.setup() - self.p['a'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['b'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) + self.p["a"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["b"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['multiply_divide_comp.multdiv_output'] + a = self.p["a"] + b = self.p["b"] + out = self.p["multiply_divide_comp.multdiv_output"] expected = a * b - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestElementMultiplyDivideMultipleInputs(unittest.TestCase): +class TestElementMultiplyDivideMultipleInputs(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) - ivc.add_output(name='c', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) + ivc.add_output(name="c", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3) + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation("multdiv_output", ["input_a", "input_b", "input_c"], vec_size=self.nn, length=3) - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') - self.p.model.connect('c', 'multiply_divide_comp.input_c') + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") + self.p.model.connect("c", "multiply_divide_comp.input_c") self.p.setup() - self.p['a'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['b'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['c'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) + self.p["a"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["b"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["c"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - out = self.p['multiply_divide_comp.multdiv_output'] + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + out = self.p["multiply_divide_comp.multdiv_output"] expected = a * b * c - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestElementMultiplyDivideDivisionFirst(unittest.TestCase): +class TestElementMultiplyDivideDivisionFirst(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) - ivc.add_output(name='c', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) + ivc.add_output(name="c", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3,divide=[True,True,False]) + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation( + "multdiv_output", ["input_a", "input_b", "input_c"], vec_size=self.nn, length=3, divide=[True, True, False] + ) - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') - self.p.model.connect('c', 'multiply_divide_comp.input_c') + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") + self.p.model.connect("c", "multiply_divide_comp.input_c") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['b'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['c'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) + self.p["a"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["b"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["c"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - out = self.p['multiply_divide_comp.multdiv_output'] + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + out = self.p["multiply_divide_comp.multdiv_output"] expected = 1 / a / b * c - assert_near_equal(out, expected,1e-15) + assert_near_equal(out, expected, 1e-15) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestElementMultiplyDivideScalingFactor(unittest.TestCase): +class TestElementMultiplyDivideScalingFactor(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) - ivc.add_output(name='c', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) + ivc.add_output(name="c", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3,scaling_factor=2) + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation( + "multdiv_output", ["input_a", "input_b", "input_c"], vec_size=self.nn, length=3, scaling_factor=2 + ) - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') - self.p.model.connect('c', 'multiply_divide_comp.input_c') + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") + self.p.model.connect("c", "multiply_divide_comp.input_c") self.p.setup() - self.p['a'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['b'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['c'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) + self.p["a"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["b"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["c"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - out = self.p['multiply_divide_comp.multdiv_output'] + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + out = self.p["multiply_divide_comp.multdiv_output"] expected = 2 * a * b * c - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestElementMultiplyDivideUnits(unittest.TestCase): +class TestElementMultiplyDivideUnits(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3), units='kg') - ivc.add_output(name='b', shape=(self.nn, 3), units='m') - ivc.add_output(name='c', shape=(self.nn, 3), units='s ** 2') - - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) - - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3, - input_units=['kg','m','s**2'], divide=[False, False, True]) - - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') - self.p.model.connect('c', 'multiply_divide_comp.input_c') + ivc.add_output(name="a", shape=(self.nn, 3), units="kg") + ivc.add_output(name="b", shape=(self.nn, 3), units="m") + ivc.add_output(name="c", shape=(self.nn, 3), units="s ** 2") + + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) + + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation( + "multdiv_output", + ["input_a", "input_b", "input_c"], + vec_size=self.nn, + length=3, + input_units=["kg", "m", "s**2"], + divide=[False, False, True], + ) + + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") + self.p.model.connect("c", "multiply_divide_comp.input_c") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['b'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) + self.p["a"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["b"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) # use uniform 1 - 2 distribution to avoid div zero - self.p['c'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) + self.p["c"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - out = self.p.get_val('multiply_divide_comp.multdiv_output', units='kN') + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + out = self.p.get_val("multiply_divide_comp.multdiv_output", units="kN") expected = a * b / c / 1000 - assert_near_equal(out, expected,1e-8) + assert_near_equal(out, expected, 1e-8) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestElementMultiplyDivideUnits_DivideFirst(unittest.TestCase): +class TestElementMultiplyDivideUnits_DivideFirst(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3), units='kg') - ivc.add_output(name='b', shape=(self.nn, 3), units='m') - ivc.add_output(name='c', shape=(self.nn, 3), units='s ** 2') - - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) - - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_c','input_b','input_a'],vec_size=self.nn,length=3, - input_units=['s**2','m','kg'], divide=[True, False, False]) - - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') - self.p.model.connect('c', 'multiply_divide_comp.input_c') + ivc.add_output(name="a", shape=(self.nn, 3), units="kg") + ivc.add_output(name="b", shape=(self.nn, 3), units="m") + ivc.add_output(name="c", shape=(self.nn, 3), units="s ** 2") + + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) + + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation( + "multdiv_output", + ["input_c", "input_b", "input_a"], + vec_size=self.nn, + length=3, + input_units=["s**2", "m", "kg"], + divide=[True, False, False], + ) + + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") + self.p.model.connect("c", "multiply_divide_comp.input_c") self.p.setup(force_alloc_complex=True) - self.p['a'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) - self.p['b'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) + self.p["a"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) + self.p["b"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) # use uniform 1 - 2 distribution to avoid div zero - self.p['c'] = np.random.uniform(low=1,high=2,size=(self.nn, 3)) + self.p["c"] = np.random.uniform(low=1, high=2, size=(self.nn, 3)) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - c = self.p['c'] - out = self.p.get_val('multiply_divide_comp.multdiv_output', units='kN') + a = self.p["a"] + b = self.p["b"] + c = self.p["c"] + out = self.p.get_val("multiply_divide_comp.multdiv_output", units="kN") expected = a * b / c / 1000 - assert_near_equal(out, expected,1e-8) + assert_near_equal(out, expected, 1e-8) def test_partials(self): - partials = self.p.check_partials(method='cs', out_stream=None) + partials = self.p.check_partials(method="cs", out_stream=None) assert_check_partials(partials) -class TestWrongUnitsCount(unittest.TestCase): +class TestWrongUnitsCount(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) - ivc.add_output(name='c', shape=(self.nn, 3)) - - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) + ivc.add_output(name="c", shape=(self.nn, 3)) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3,input_units=['kg','ft']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') - self.p.model.connect('c', 'multiply_divide_comp.input_c') + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation( + "multdiv_output", ["input_a", "input_b", "input_c"], vec_size=self.nn, length=3, input_units=["kg", "ft"] + ) + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") + self.p.model.connect("c", "multiply_divide_comp.input_c") def test_for_exception(self): - self.assertRaises(ValueError,self.p.setup) + self.assertRaises(ValueError, self.p.setup) -class TestWrongDivideCount(unittest.TestCase): +class TestWrongDivideCount(unittest.TestCase): def setUp(self): self.nn = 5 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a', shape=(self.nn, 3)) - ivc.add_output(name='b', shape=(self.nn, 3)) - ivc.add_output(name='c', shape=(self.nn, 3)) + ivc.add_output(name="a", shape=(self.nn, 3)) + ivc.add_output(name="b", shape=(self.nn, 3)) + ivc.add_output(name="c", shape=(self.nn, 3)) - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['a', 'b','c']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b", "c"]) - multi=self.p.model.add_subsystem(name='multiply_divide_comp', - subsys=ElementMultiplyDivideComp()) - multi.add_equation('multdiv_output',['input_a','input_b','input_c'],vec_size=self.nn,length=3,divide=[False,True]) - - self.p.model.connect('a', 'multiply_divide_comp.input_a') - self.p.model.connect('b', 'multiply_divide_comp.input_b') - self.p.model.connect('c', 'multiply_divide_comp.input_c') + multi = self.p.model.add_subsystem(name="multiply_divide_comp", subsys=ElementMultiplyDivideComp()) + multi.add_equation( + "multdiv_output", ["input_a", "input_b", "input_c"], vec_size=self.nn, length=3, divide=[False, True] + ) + self.p.model.connect("a", "multiply_divide_comp.input_a") + self.p.model.connect("b", "multiply_divide_comp.input_b") + self.p.model.connect("c", "multiply_divide_comp.input_c") def test_for_exception(self): - self.assertRaises(ValueError,self.p.setup) + self.assertRaises(ValueError, self.p.setup) -class TestForDocs(unittest.TestCase): +class TestForDocs(unittest.TestCase): def test(self): """ A simple example to compute inertial forces on four projectiles at @@ -459,35 +454,39 @@ def test(self): p = Problem(model=Group()) ivc = IndepVarComp() - #the vector represents forces at 3 analysis points (rows) in 2 dimensional plane (cols) - ivc.add_output(name='mass', shape=(n,length), units='kg') - ivc.add_output(name='acceleration', shape=(n,length), units='m / s**2') - p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['mass', 'acceleration']) - - #construct an multi/subtracter here. create a relationship through the add_equation method - multi = ElementMultiplyDivideComp() - multi.add_equation('inertial_force',input_names=['mass', 'acceleration'],vec_size=n,length=length, input_units=['kg','m / s**2'], scaling_factor=-1) - #note the scaling factors. we assume all forces are positive sign upstream + # the vector represents forces at 3 analysis points (rows) in 2 dimensional plane (cols) + ivc.add_output(name="mass", shape=(n, length), units="kg") + ivc.add_output(name="acceleration", shape=(n, length), units="m / s**2") + p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["mass", "acceleration"]) - p.model.add_subsystem(name='inertialforcecomp', subsys=multi, promotes_inputs=['*']) + # construct an multi/subtracter here. create a relationship through the add_equation method + multi = ElementMultiplyDivideComp() + multi.add_equation( + "inertial_force", + input_names=["mass", "acceleration"], + vec_size=n, + length=length, + input_units=["kg", "m / s**2"], + scaling_factor=-1, + ) + # note the scaling factors. we assume all forces are positive sign upstream + + p.model.add_subsystem(name="inertialforcecomp", subsys=multi, promotes_inputs=["*"]) p.setup() - #set thrust to exceed drag, weight to equal lift for this scenario - p['mass'] = np.ones((n,length)) * 500 - p['acceleration'] = np.random.rand(n,length) - + # set thrust to exceed drag, weight to equal lift for this scenario + p["mass"] = np.ones((n, length)) * 500 + p["acceleration"] = np.random.rand(n, length) p.run_model() # print(p.get_val('totalforcecomp.total_force', units='kN')) # Verify the results - expected_i = - p['mass'] * p['acceleration'] / 1000 - assert_near_equal(p.get_val('inertialforcecomp.inertial_force', units='kN'), expected_i) + expected_i = -p["mass"] * p["acceleration"] / 1000 + assert_near_equal(p.get_val("inertialforcecomp.inertial_force", units="kN"), expected_i) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/openconcept/utilities/math/tests/test_old_integrals.py b/openconcept/utilities/math/tests/test_old_integrals.py index 61e9f5cf..f5c261c9 100644 --- a/openconcept/utilities/math/tests/test_old_integrals.py +++ b/openconcept/utilities/math/tests/test_old_integrals.py @@ -5,6 +5,7 @@ from openmdao.api import IndepVarComp, Group, Problem from openconcept.utilities.math.integrals import OldIntegrator + class MultiPhaseIntegratorTestGroup(Group): """An OpenMDAO group to test the every-node integrator component @@ -24,62 +25,73 @@ class MultiPhaseIntegratorTestGroup(Group): """ def initialize(self): - self.options.declare('segment_names', default=None, desc="Names of differentiation segments") - self.options.declare('segments_to_count', default=None, desc="Names of differentiation segments") - self.options.declare('quantity_units',default=None, desc="Units of the quantity being differentiated") - self.options.declare('diff_units',default=None, desc="Units of the differential") - self.options.declare('num_nodes',default=11, desc="Number of nodes per segment") - self.options.declare('integrator',default='simpson', desc="Which simpson integrator to use") - self.options.declare('time_setup',default='dt') + self.options.declare("segment_names", default=None, desc="Names of differentiation segments") + self.options.declare("segments_to_count", default=None, desc="Names of differentiation segments") + self.options.declare("quantity_units", default=None, desc="Units of the quantity being differentiated") + self.options.declare("diff_units", default=None, desc="Units of the differential") + self.options.declare("num_nodes", default=11, desc="Number of nodes per segment") + self.options.declare("integrator", default="simpson", desc="Which simpson integrator to use") + self.options.declare("time_setup", default="dt") def setup(self): - segment_names = self.options['segment_names'] - segments_to_count = self.options['segments_to_count'] - quantity_units = self.options['quantity_units'] - diff_units = self.options['diff_units'] - num_nodes = self.options['num_nodes'] - integrator_option = self.options['integrator'] - time_setup = self.options['time_setup'] + segment_names = self.options["segment_names"] + segments_to_count = self.options["segments_to_count"] + quantity_units = self.options["quantity_units"] + diff_units = self.options["diff_units"] + num_nodes = self.options["num_nodes"] + integrator_option = self.options["integrator"] + time_setup = self.options["time_setup"] if segment_names is None: nn_tot = num_nodes else: nn_tot = num_nodes * len(segment_names) - iv = self.add_subsystem('iv', IndepVarComp()) - - self.add_subsystem('integral', OldIntegrator(segment_names=segment_names, segments_to_count=segments_to_count, quantity_units=quantity_units, - diff_units=diff_units, num_nodes=num_nodes, method=integrator_option, time_setup=time_setup)) + iv = self.add_subsystem("iv", IndepVarComp()) + + self.add_subsystem( + "integral", + OldIntegrator( + segment_names=segment_names, + segments_to_count=segments_to_count, + quantity_units=quantity_units, + diff_units=diff_units, + num_nodes=num_nodes, + method=integrator_option, + time_setup=time_setup, + ), + ) if quantity_units is None and diff_units is None: rate_units = None elif quantity_units is None: - rate_units = '(' + diff_units +')** -1' + rate_units = "(" + diff_units + ")** -1" elif diff_units is None: rate_units = quantity_units else: - rate_units = '('+quantity_units+') / (' + diff_units +')' + rate_units = "(" + quantity_units + ") / (" + diff_units + ")" - iv.add_output('rate_to_integrate', val=np.ones((nn_tot,)), units=rate_units) - iv.add_output('initial_value', val=0, units=quantity_units) + iv.add_output("rate_to_integrate", val=np.ones((nn_tot,)), units=rate_units) + iv.add_output("initial_value", val=0, units=quantity_units) - self.connect('iv.rate_to_integrate','integral.dqdt') - self.connect('iv.initial_value', 'integral.q_initial') + self.connect("iv.rate_to_integrate", "integral.dqdt") + self.connect("iv.initial_value", "integral.q_initial") if segment_names is None: - if time_setup == 'dt': - iv.add_output('dt', val=1, units=diff_units) - self.connect('iv.dt', 'integral.dt') - elif time_setup == 'duration': - iv.add_output('duration', val=1*(nn_tot-1), units=diff_units) - self.connect('iv.duration', 'integral.duration') - elif time_setup == 'bounds': - iv.add_output('t_initial', val=2, units=diff_units) - iv.add_output('t_final', val=2 + 1*(nn_tot-1), units=diff_units) - self.connect('iv.t_initial','integral.t_initial') - self.connect('iv.t_final','integral.t_final') + if time_setup == "dt": + iv.add_output("dt", val=1, units=diff_units) + self.connect("iv.dt", "integral.dt") + elif time_setup == "duration": + iv.add_output("duration", val=1 * (nn_tot - 1), units=diff_units) + self.connect("iv.duration", "integral.duration") + elif time_setup == "bounds": + iv.add_output("t_initial", val=2, units=diff_units) + iv.add_output("t_final", val=2 + 1 * (nn_tot - 1), units=diff_units) + self.connect("iv.t_initial", "integral.t_initial") + self.connect("iv.t_final", "integral.t_final") else: for segment_name in segment_names: - iv.add_output(segment_name + '|dt', val=1, units=diff_units) - self.connect('iv.'+segment_name + '|dt','integral.'+segment_name + '|dt') + iv.add_output(segment_name + "|dt", val=1, units=diff_units) + self.connect("iv." + segment_name + "|dt", "integral." + segment_name + "|dt") + class IntegratorEveryNodeCommonTestCases(object): """ @@ -92,261 +104,321 @@ def test_uniform_single_phase_no_units(self): prob.run_model() num_nodes = self.num_nodes nn_tot = num_nodes - assert_near_equal(prob['integral.q'], np.linspace(0, nn_tot-1, nn_tot), tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), nn_tot-1, tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["integral.q"], np.linspace(0, nn_tot - 1, nn_tot), tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), nn_tot - 1, tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_linear_single_phase_no_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) + x = np.linspace(0, nn_tot - 1, nn_tot) fprime = x - f = x ** 2 / 2 + f = x**2 / 2 prob = Problem(MultiPhaseIntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator)) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob['integral.q'], f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["integral.q"], f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_single_phase_no_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x prob = Problem(MultiPhaseIntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator)) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob['integral.q'], f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob["integral.q"], f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_single_phase_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(MultiPhaseIntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s')) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + MultiPhaseIntegratorTestGroup( + num_nodes=self.num_nodes, integrator=self.integrator, quantity_units="kg", diff_units="s" + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_duration(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(MultiPhaseIntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s',time_setup='duration')) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + MultiPhaseIntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + time_setup="duration", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) @pytest.mark.filterwarnings("ignore:Input*:UserWarning") def test_quadratic_bounds(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - - prob = Problem(MultiPhaseIntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s',time_setup='bounds')) + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + + prob = Problem( + MultiPhaseIntegratorTestGroup( + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + time_setup="bounds", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_single_phase_no_rate_units(self): num_nodes = self.num_nodes nn_tot = num_nodes - x = np.linspace(0, nn_tot-1, nn_tot) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + x = np.linspace(0, nn_tot - 1, nn_tot) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x - prob = Problem(MultiPhaseIntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, - diff_units='s')) + prob = Problem( + MultiPhaseIntegratorTestGroup(num_nodes=self.num_nodes, integrator=self.integrator, diff_units="s") + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units=None), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units=None), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units=None), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units=None), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_three_phase_units_equal_dt(self): num_nodes = self.num_nodes nn_tot = num_nodes - x1 = np.linspace(0, nn_tot-1, nn_tot) - x2 = np.linspace(nn_tot-1, 2*(nn_tot-1), nn_tot) - x3 = np.linspace(2*(nn_tot-1), 3*(nn_tot-1), nn_tot) + x1 = np.linspace(0, nn_tot - 1, nn_tot) + x2 = np.linspace(nn_tot - 1, 2 * (nn_tot - 1), nn_tot) + x3 = np.linspace(2 * (nn_tot - 1), 3 * (nn_tot - 1), nn_tot) x = np.concatenate([x1, x2, x3]) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - prob = Problem(MultiPhaseIntegratorTestGroup(segment_names=['climb','cruise','descent'], - num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s')) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + prob = Problem( + MultiPhaseIntegratorTestGroup( + segment_names=["climb", "cruise", "descent"], + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_three_phase_units_equal_dt_count_all(self): num_nodes = self.num_nodes nn_tot = num_nodes - x1 = np.linspace(0, nn_tot-1, nn_tot) - x2 = np.linspace(nn_tot-1, 2*(nn_tot-1), nn_tot) - x3 = np.linspace(2*(nn_tot-1), 3*(nn_tot-1), nn_tot) + x1 = np.linspace(0, nn_tot - 1, nn_tot) + x2 = np.linspace(nn_tot - 1, 2 * (nn_tot - 1), nn_tot) + x3 = np.linspace(2 * (nn_tot - 1), 3 * (nn_tot - 1), nn_tot) x = np.concatenate([x1, x2, x3]) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - prob = Problem(MultiPhaseIntegratorTestGroup(segment_names=['climb','cruise','descent'], - segments_to_count=['climb','cruise','descent'], - num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s')) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + prob = Problem( + MultiPhaseIntegratorTestGroup( + segment_names=["climb", "cruise", "descent"], + segments_to_count=["climb", "cruise", "descent"], + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_three_phase_units_equal_dt_skip_middle(self): num_nodes = self.num_nodes nn_tot = num_nodes - x1 = np.linspace(0, nn_tot-1, nn_tot) - x2 = np.linspace(nn_tot-1, 2*(nn_tot-1), nn_tot) - x3 = np.linspace(2*(nn_tot-1), 3*(nn_tot-1), nn_tot) + x1 = np.linspace(0, nn_tot - 1, nn_tot) + x2 = np.linspace(nn_tot - 1, 2 * (nn_tot - 1), nn_tot) + x3 = np.linspace(2 * (nn_tot - 1), 3 * (nn_tot - 1), nn_tot) x = np.concatenate([x1, x2, x3]) - fprime = 4 * x **2 - 8*x + 5 - fint = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - f = np.concatenate([fint[:nn_tot], - np.ones((nn_tot,))*fint[nn_tot], - fint[2*nn_tot:] - np.ones((nn_tot,))*(fint[2*nn_tot]-fint[nn_tot])]) - prob = Problem(MultiPhaseIntegratorTestGroup(segment_names=['climb','cruise','descent'], - segments_to_count=['climb','descent'], - num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s')) + fprime = 4 * x**2 - 8 * x + 5 + fint = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + f = np.concatenate( + [ + fint[:nn_tot], + np.ones((nn_tot,)) * fint[nn_tot], + fint[2 * nn_tot :] - np.ones((nn_tot,)) * (fint[2 * nn_tot] - fint[nn_tot]), + ] + ) + prob = Problem( + MultiPhaseIntegratorTestGroup( + segment_names=["climb", "cruise", "descent"], + segments_to_count=["climb", "descent"], + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime + prob["iv.rate_to_integrate"] = fprime prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_three_phase_units_unequal_dt(self): num_nodes = self.num_nodes nn_tot = num_nodes - x1 = np.linspace(0, nn_tot-1, nn_tot) - x2 = np.linspace(nn_tot-1, 3*(nn_tot-1), nn_tot) - x3 = np.linspace(3*(nn_tot-1), 6*(nn_tot-1), nn_tot) + x1 = np.linspace(0, nn_tot - 1, nn_tot) + x2 = np.linspace(nn_tot - 1, 3 * (nn_tot - 1), nn_tot) + x3 = np.linspace(3 * (nn_tot - 1), 6 * (nn_tot - 1), nn_tot) x = np.concatenate([x1, x2, x3]) - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x - prob = Problem(MultiPhaseIntegratorTestGroup(segment_names=['climb','cruise','descent'], - num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s')) + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + prob = Problem( + MultiPhaseIntegratorTestGroup( + segment_names=["climb", "cruise", "descent"], + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime - prob['iv.climb|dt'] = 1 - prob['iv.cruise|dt'] = 2 - prob['iv.descent|dt'] = 3 + prob["iv.rate_to_integrate"] = fprime + prob["iv.climb|dt"] = 1 + prob["iv.cruise|dt"] = 2 + prob["iv.descent|dt"] = 3 prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_three_phase_units_unequal_dt_initial_val(self): num_nodes = self.num_nodes nn_tot = num_nodes - x1 = np.linspace(0, nn_tot-1, nn_tot) - x2 = np.linspace(nn_tot-1, 3*(nn_tot-1), nn_tot) - x3 = np.linspace(3*(nn_tot-1), 6*(nn_tot-1), nn_tot) + x1 = np.linspace(0, nn_tot - 1, nn_tot) + x2 = np.linspace(nn_tot - 1, 3 * (nn_tot - 1), nn_tot) + x3 = np.linspace(3 * (nn_tot - 1), 6 * (nn_tot - 1), nn_tot) x = np.concatenate([x1, x2, x3]) - C = 10. - fprime = 4 * x **2 - 8*x + 5 - f = 4 * x ** 3 / 3 - 8 * x ** 2 / 2 + 5*x + C - prob = Problem(MultiPhaseIntegratorTestGroup(segment_names=['climb','cruise','descent'], - num_nodes=self.num_nodes, integrator=self.integrator, - quantity_units='kg', diff_units='s')) + C = 10.0 + fprime = 4 * x**2 - 8 * x + 5 + f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x + C + prob = Problem( + MultiPhaseIntegratorTestGroup( + segment_names=["climb", "cruise", "descent"], + num_nodes=self.num_nodes, + integrator=self.integrator, + quantity_units="kg", + diff_units="s", + ) + ) prob.setup(check=True, force_alloc_complex=True) - prob['iv.rate_to_integrate'] = fprime - prob['iv.climb|dt'] = 1 - prob['iv.cruise|dt'] = 2 - prob['iv.descent|dt'] = 3 - prob['iv.initial_value'] = C + prob["iv.rate_to_integrate"] = fprime + prob["iv.climb|dt"] = 1 + prob["iv.cruise|dt"] = 2 + prob["iv.descent|dt"] = 3 + prob["iv.initial_value"] = C prob.run_model() - assert_near_equal(prob.get_val('integral.q', units='kg'), f, tolerance=1e-14) - assert_near_equal(prob.get_val('integral.q_final', units='kg'), f[-1], tolerance=1e-14) - partials = prob.check_partials(method='cs',compact_print=True) + assert_near_equal(prob.get_val("integral.q", units="kg"), f, tolerance=1e-14) + assert_near_equal(prob.get_val("integral.q_final", units="kg"), f[-1], tolerance=1e-14) + partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials, atol=1e-8, rtol=1e0) + class SimpsonIntegratorEveryNode5PtTestCases(unittest.TestCase, IntegratorEveryNodeCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 11 - self.integrator = 'simpson' + self.integrator = "simpson" super(SimpsonIntegratorEveryNode5PtTestCases, self).__init__(*args, **kwargs) + class SimpsonIntegratorEveryNode3PtTestCases(unittest.TestCase, IntegratorEveryNodeCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 7 - self.integrator = 'simpson' + self.integrator = "simpson" super(SimpsonIntegratorEveryNode3PtTestCases, self).__init__(*args, **kwargs) + class BDFIntegratorEveryNode5PtTestCases(unittest.TestCase, IntegratorEveryNodeCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 11 - self.integrator = 'bdf3' + self.integrator = "bdf3" super(BDFIntegratorEveryNode5PtTestCases, self).__init__(*args, **kwargs) + class BDFIntegratorEveryNode3PtTestCases(unittest.TestCase, IntegratorEveryNodeCommonTestCases): """ Only run the common test cases using second order accuracy because it cannot differentiate the quartic accurately """ + def __init__(self, *args, **kwargs): self.num_nodes = 7 - self.integrator = 'bdf3' - super(BDFIntegratorEveryNode3PtTestCases, self).__init__(*args, **kwargs) \ No newline at end of file + self.integrator = "bdf3" + super(BDFIntegratorEveryNode3PtTestCases, self).__init__(*args, **kwargs) diff --git a/openconcept/utilities/selector.py b/openconcept/utilities/selector.py index 7a5f6fb2..f3107497 100644 --- a/openconcept/utilities/selector.py +++ b/openconcept/utilities/selector.py @@ -1,6 +1,7 @@ import openmdao.api as om import numpy as np + class SelectorComp(om.ExplicitComponent): """ Selects an output from the set of user-specified inputs @@ -26,12 +27,12 @@ class SelectorComp(om.ExplicitComponent): user defined inputs : any The data inputs must be specified by the user using the input_names option and all inputs must have the same units, if none are specified error is raised (vector) - + Outputs ------- result : same as selected input The same value as the input selected by the selector input (vector) - + Options ------- num_nodes : int @@ -39,51 +40,52 @@ class SelectorComp(om.ExplicitComponent): input_names : iterable of strings List of the names of the the user-specified inputs units : string - OpenMDAO-style units of the inputs; all inputs should have these units + OpenMDAO-style units of the inputs; all inputs should have these units """ + def initialize(self): - self.options.declare('num_nodes', default=1, desc='Length of all input and output arrays') - self.options.declare('input_names', default=[], desc='List of input names') - self.options.declare('units', default=None, desc='Units of inputs (should all be the same units)') + self.options.declare("num_nodes", default=1, desc="Length of all input and output arrays") + self.options.declare("input_names", default=[], desc="List of input names") + self.options.declare("units", default=None, desc="Units of inputs (should all be the same units)") def setup(self): - nn = self.options['num_nodes'] - names = list(self.options['input_names']) - unit = self.options['units'] + nn = self.options["num_nodes"] + names = list(self.options["input_names"]) + unit = self.options["units"] # Add user-specified inputs if len(names) < 1: - raise ValueError('input_names option must have at least one input name') + raise ValueError("input_names option must have at least one input name") for name in names: self.add_input(name, shape=(nn,), units=unit) - - self.add_input('selector', np.zeros(nn, dtype=int), shape=(nn,)) - self.add_output('result', shape=(nn,), units=unit) + + self.add_input("selector", np.zeros(nn, dtype=int), shape=(nn,)) + self.add_output("result", shape=(nn,), units=unit) arange = np.arange(0, nn) - self.declare_partials('result', names, rows=arange, cols=arange) - + self.declare_partials("result", names, rows=arange, cols=arange) + def compute(self, inputs, outputs): - input_names = list(self.options['input_names']) + input_names = list(self.options["input_names"]) num_inputs = len(input_names) - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] - outputs['result'] = np.zeros((nn,)) - selector = np.around(inputs['selector']) + outputs["result"] = np.zeros((nn,)) + selector = np.around(inputs["selector"]) if np.any(selector < 0) or np.any(selector >= num_inputs): - raise RuntimeWarning('selector input values must be in the range [0, # of inputs)') + raise RuntimeWarning("selector input values must be in the range [0, # of inputs)") for i_input in range(num_inputs): mask = np.where(selector == i_input, 1, 0) - outputs['result'] += inputs[input_names[i_input]] * mask - + outputs["result"] += inputs[input_names[i_input]] * mask + def compute_partials(self, inputs, J): - input_names = list(self.options['input_names']) + input_names = list(self.options["input_names"]) num_inputs = len(input_names) - nn = self.options['num_nodes'] + nn = self.options["num_nodes"] - selector = np.around(inputs['selector']) + selector = np.around(inputs["selector"]) for i_input, input_name in enumerate(input_names): - J['result', input_name] = np.where(selector == i_input, 1, 0) \ No newline at end of file + J["result", input_name] = np.where(selector == i_input, 1, 0) diff --git a/openconcept/utilities/tests/test_dict_indepvarcomp.py b/openconcept/utilities/tests/test_dict_indepvarcomp.py index a17029d6..4855fc83 100644 --- a/openconcept/utilities/tests/test_dict_indepvarcomp.py +++ b/openconcept/utilities/tests/test_dict_indepvarcomp.py @@ -12,80 +12,83 @@ aero = dict() ## clmax data =========================== clmax = dict() -clmax['flaps30'] = {'value': 1.7} -clmax['flaps10'] = {'value': 1.5} -aero['CLmax'] = clmax +clmax["flaps30"] = {"value": 1.7} +clmax["flaps10"] = {"value": 1.5} +aero["CLmax"] = clmax ## other aero data ====================== -aero['myvector'] = {'value' : np.ones(10), 'units' : 'kg'} +aero["myvector"] = {"value": np.ones(10), "units": "kg"} geom = dict() -geom['S_ref'] = {'value' : 20, 'units': 'ft**2'} -geom['noval'] = {'units' : 'ft**2'} +geom["S_ref"] = {"value": 20, "units": "ft**2"} +geom["noval"] = {"units": "ft**2"} data = dict() -data['aero'] = aero -data['geom'] = geom +data["aero"] = aero +data["geom"] = geom class TestOne(unittest.TestCase): - def setUp(self): self.nn = 3 self.p = Problem(model=Group()) ivc = DictIndepVarComp(data_dict=data) - ivc.add_output(name='a', shape=(self.nn,)) - ivc.add_output(name='b', shape=(self.nn,)) - ivc.add_output_from_dict('geom|S_ref') - ivc.add_output_from_dict('aero|CLmax|flaps30') - ivc.add_output_from_dict('aero|CLmax|flaps10') + ivc.add_output(name="a", shape=(self.nn,)) + ivc.add_output(name="b", shape=(self.nn,)) + ivc.add_output_from_dict("geom|S_ref") + ivc.add_output_from_dict("aero|CLmax|flaps30") + ivc.add_output_from_dict("aero|CLmax|flaps10") - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['*']) + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["*"]) self.p.setup() - self.p['a'] = np.random.rand(self.nn,) - self.p['b'] = np.random.rand(self.nn,) + self.p["a"] = np.random.rand( + self.nn, + ) + self.p["b"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a = self.p['a'] - b = self.p['b'] - out = self.p['geom|S_ref'] + a = self.p["a"] + b = self.p["b"] + out = self.p["geom|S_ref"] expected = 20 - assert_near_equal(out, expected,1e-16) + assert_near_equal(out, expected, 1e-16) def test_units(self): - out = self.p.get_val('geom|S_ref', units='m**2') + out = self.p.get_val("geom|S_ref", units="m**2") expected = 20 * 0.3048**2 - assert_near_equal(out, expected,1e-4) + assert_near_equal(out, expected, 1e-4) def test_twodeep(self): - out = self.p.get_val('aero|CLmax|flaps30') + out = self.p.get_val("aero|CLmax|flaps30") expected = 1.7 - assert_near_equal(out, expected,1e-4) - out = self.p.get_val('aero|CLmax|flaps10') + assert_near_equal(out, expected, 1e-4) + out = self.p.get_val("aero|CLmax|flaps10") expected = 1.5 - assert_near_equal(out, expected,1e-4) + assert_near_equal(out, expected, 1e-4) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) + class TestAddNonexistentVar(unittest.TestCase): def setUp(self): self.nn = 3 self.p = Problem(model=Group()) self.ivc = DictIndepVarComp(data_dict=data) - self.ivc.add_output(name='a', shape=(self.nn,)) - self.ivc.add_output(name='b', shape=(self.nn,)) + self.ivc.add_output(name="a", shape=(self.nn,)) + self.ivc.add_output(name="b", shape=(self.nn,)) def test_nonexistent_key(self): - self.assertRaises(KeyError,self.ivc.add_output_from_dict,'geom|S_ref_blah') + self.assertRaises(KeyError, self.ivc.add_output_from_dict, "geom|S_ref_blah") def test_no_value(self): - self.assertRaises(KeyError,self.ivc.add_output_from_dict,'geom|noval') + self.assertRaises(KeyError, self.ivc.add_output_from_dict, "geom|noval") + # class TestAddSubtractCompNx1(unittest.TestCase): @@ -382,5 +385,5 @@ def test_no_value(self): # assert_near_equal(p.get_val('totalforcecomp.total_force', units='kN'), expected_i) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/openconcept/utilities/tests/test_dvlabel.py b/openconcept/utilities/tests/test_dvlabel.py index 51de7383..dcdd9cdf 100644 --- a/openconcept/utilities/tests/test_dvlabel.py +++ b/openconcept/utilities/tests/test_dvlabel.py @@ -8,115 +8,110 @@ from openconcept.utilities import DVLabel from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -class TestBasic(unittest.TestCase): +class TestBasic(unittest.TestCase): def setUp(self): self.nn = 3 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a_to_be_renamed', shape=(self.nn,)) - ivc.add_output(name='b_to_be_renamed', shape=(self.nn,)) - dvlabel = DVLabel([['a_to_be_renamed','a',np.ones(self.nn),None], - ['b_to_be_renamed','b',np.ones(self.nn),None]]) - - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['*']) - self.p.model.add_subsystem(name='dvlabel', - subsys=dvlabel, - promotes_inputs=['*'], - promotes_outputs=['*']) + ivc.add_output(name="a_to_be_renamed", shape=(self.nn,)) + ivc.add_output(name="b_to_be_renamed", shape=(self.nn,)) + dvlabel = DVLabel( + [["a_to_be_renamed", "a", np.ones(self.nn), None], ["b_to_be_renamed", "b", np.ones(self.nn), None]] + ) + + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["*"]) + self.p.model.add_subsystem(name="dvlabel", subsys=dvlabel, promotes_inputs=["*"], promotes_outputs=["*"]) self.p.setup() - self.p['a_to_be_renamed'] = np.random.rand(self.nn,) - self.p['b_to_be_renamed'] = np.random.rand(self.nn,) + self.p["a_to_be_renamed"] = np.random.rand( + self.nn, + ) + self.p["b_to_be_renamed"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a_in = self.p['a_to_be_renamed'] - b_in = self.p['b_to_be_renamed'] - a_out = self.p['a'] - b_out = self.p['b'] - assert_near_equal(a_in, a_out,1e-16) - assert_near_equal(b_in, b_out,1e-16) + a_in = self.p["a_to_be_renamed"] + b_in = self.p["b_to_be_renamed"] + a_out = self.p["a"] + b_out = self.p["b"] + assert_near_equal(a_in, a_out, 1e-16) + assert_near_equal(b_in, b_out, 1e-16) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestUnits(unittest.TestCase): +class TestUnits(unittest.TestCase): def setUp(self): self.nn = 3 self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a_to_be_renamed', shape=(self.nn,), units='m/s') - ivc.add_output(name='b_to_be_renamed', shape=(self.nn,), units='kg') - dvlabel = DVLabel([['a_to_be_renamed','a',np.ones(self.nn),'m/s'], - ['b_to_be_renamed','b',np.ones(self.nn),'lbm']]) - - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['*']) - self.p.model.add_subsystem(name='dvlabel', - subsys=dvlabel, - promotes_inputs=['*'], - promotes_outputs=['*']) + ivc.add_output(name="a_to_be_renamed", shape=(self.nn,), units="m/s") + ivc.add_output(name="b_to_be_renamed", shape=(self.nn,), units="kg") + dvlabel = DVLabel( + [["a_to_be_renamed", "a", np.ones(self.nn), "m/s"], ["b_to_be_renamed", "b", np.ones(self.nn), "lbm"]] + ) + + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["*"]) + self.p.model.add_subsystem(name="dvlabel", subsys=dvlabel, promotes_inputs=["*"], promotes_outputs=["*"]) self.p.setup() - self.p['a_to_be_renamed'] = np.random.rand(self.nn,) - self.p['b_to_be_renamed'] = np.random.rand(self.nn,) + self.p["a_to_be_renamed"] = np.random.rand( + self.nn, + ) + self.p["b_to_be_renamed"] = np.random.rand( + self.nn, + ) self.p.run_model() def test_results(self): - a_in = self.p['a_to_be_renamed'] - b_in = self.p['b_to_be_renamed'] - a_out = self.p['a'] - b_out = self.p['b'] - assert_near_equal(a_in, a_out,1e-16) - assert_near_equal(b_in*2.20462, b_out,1e-5) + a_in = self.p["a_to_be_renamed"] + b_in = self.p["b_to_be_renamed"] + a_out = self.p["a"] + b_out = self.p["b"] + assert_near_equal(a_in, a_out, 1e-16) + assert_near_equal(b_in * 2.20462, b_out, 1e-5) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -class TestScalars(unittest.TestCase): +class TestScalars(unittest.TestCase): def setUp(self): self.p = Problem(model=Group()) ivc = IndepVarComp() - ivc.add_output(name='a_to_be_renamed', units='m/s') - ivc.add_output(name='b_to_be_renamed', units='kg') - dvlabel = DVLabel([['a_to_be_renamed','a',1,'m/s'], - ['b_to_be_renamed','b',1,'lbm']]) - - self.p.model.add_subsystem(name='ivc', - subsys=ivc, - promotes_outputs=['*']) - self.p.model.add_subsystem(name='dvlabel', - subsys=dvlabel, - promotes_inputs=['*'], - promotes_outputs=['*']) + ivc.add_output(name="a_to_be_renamed", units="m/s") + ivc.add_output(name="b_to_be_renamed", units="kg") + dvlabel = DVLabel([["a_to_be_renamed", "a", 1, "m/s"], ["b_to_be_renamed", "b", 1, "lbm"]]) + + self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["*"]) + self.p.model.add_subsystem(name="dvlabel", subsys=dvlabel, promotes_inputs=["*"], promotes_outputs=["*"]) self.p.setup() - self.p['a_to_be_renamed'] = np.random.rand(1) - self.p['b_to_be_renamed'] = np.random.rand(1) + self.p["a_to_be_renamed"] = np.random.rand(1) + self.p["b_to_be_renamed"] = np.random.rand(1) self.p.run_model() def test_results(self): - a_in = self.p['a_to_be_renamed'] - b_in = self.p['b_to_be_renamed'] - a_out = self.p['a'] - b_out = self.p['b'] - assert_near_equal(a_in, a_out,1e-16) - assert_near_equal(b_in*2.20462, b_out,1e-5) + a_in = self.p["a_to_be_renamed"] + b_in = self.p["b_to_be_renamed"] + a_out = self.p["a"] + b_out = self.p["b"] + assert_near_equal(a_in, a_out, 1e-16) + assert_near_equal(b_in * 2.20462, b_out, 1e-5) def test_partials(self): - partials = self.p.check_partials(method='fd', out_stream=None) + partials = self.p.check_partials(method="fd", out_stream=None) assert_check_partials(partials) -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main() diff --git a/openconcept/utilities/tests/test_selector.py b/openconcept/utilities/tests/test_selector.py index fd96433e..5d44d415 100644 --- a/openconcept/utilities/tests/test_selector.py +++ b/openconcept/utilities/tests/test_selector.py @@ -4,95 +4,98 @@ from openmdao.api import Problem from openconcept.utilities import SelectorComp + class SelectorCompTestCase(unittest.TestCase): """ Test the SelectorComp component """ + def test_zero_inputs(self): p = Problem() - p.model.add_subsystem('select', SelectorComp(input_names=[]), promotes=['*']) + p.model.add_subsystem("select", SelectorComp(input_names=[]), promotes=["*"]) with self.assertRaises(ValueError): p.setup() def test_one_input(self): p = Problem() - p.model.add_subsystem('select', SelectorComp(input_names=['A']), promotes=['*']) + p.model.add_subsystem("select", SelectorComp(input_names=["A"]), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('A', np.array([5.7])) - p.set_val('selector', np.array([0])) + p.set_val("A", np.array([5.7])) + p.set_val("selector", np.array([0])) p.run_model() - assert_near_equal(p['result'], np.array([5.7])) + assert_near_equal(p["result"], np.array([5.7])) - partials = p.check_partials(method='cs',compact_print=True) + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - p.set_val('selector', np.array([1])) + p.set_val("selector", np.array([1])) with self.assertRaises(RuntimeWarning): p.run_model() - + def test_two_inputs(self): nn = 5 p = Problem() - p.model.add_subsystem('select', SelectorComp(num_nodes=nn, input_names=['A', 'B']), promotes=['*']) + p.model.add_subsystem("select", SelectorComp(num_nodes=nn, input_names=["A", "B"]), promotes=["*"]) p.setup(check=True, force_alloc_complex=True) - p.set_val('A', np.array([5.7, 2.3, -10., 42., 77.])) - p.set_val('B', np.array([-1., -1., -1., -1., -2.])) - p.set_val('selector', np.array([0, 1, 1, 0, 1])) + p.set_val("A", np.array([5.7, 2.3, -10.0, 42.0, 77.0])) + p.set_val("B", np.array([-1.0, -1.0, -1.0, -1.0, -2.0])) + p.set_val("selector", np.array([0, 1, 1, 0, 1])) p.run_model() - assert_near_equal(p['result'], np.array([5.7, -1., -1., 42., -2.])) - - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal(p["result"], np.array([5.7, -1.0, -1.0, 42.0, -2.0])) + + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - p.set_val('A', np.ones(nn)) - p.set_val('B', np.zeros(nn)) - p.set_val('selector', np.zeros(nn)) + p.set_val("A", np.ones(nn)) + p.set_val("B", np.zeros(nn)) + p.set_val("selector", np.zeros(nn)) p.run_model() - assert_near_equal(p['result'], np.ones(nn)) + assert_near_equal(p["result"], np.ones(nn)) - p.set_val('selector', np.array([0, 1, -1, 0, 0])) + p.set_val("selector", np.array([0, 1, -1, 0, 0])) with self.assertRaises(RuntimeWarning): p.run_model() - - p.set_val('selector', np.array([0, 1, 2, 0, 0])) + + p.set_val("selector", np.array([0, 1, 2, 0, 0])) with self.assertRaises(RuntimeWarning): p.run_model() - + def test_three_inputs(self): nn = 5 p = Problem() - p.model.add_subsystem('selector', SelectorComp(num_nodes=nn, input_names=['A', 'B', 'C'], units='g'), - promotes=['*']) + p.model.add_subsystem( + "selector", SelectorComp(num_nodes=nn, input_names=["A", "B", "C"], units="g"), promotes=["*"] + ) p.setup(check=True, force_alloc_complex=True) - p.set_val('A', np.array([5.7, 2.3, -10., 2., 77.]), units='g') - p.set_val('B', np.array([-1., -1., -1., -1., -2.]), units='kg') - p.set_val('C', 42.*np.ones(nn), units='g') - p.set_val('selector', np.array([0, 1, 2, 0, 2])) + p.set_val("A", np.array([5.7, 2.3, -10.0, 2.0, 77.0]), units="g") + p.set_val("B", np.array([-1.0, -1.0, -1.0, -1.0, -2.0]), units="kg") + p.set_val("C", 42.0 * np.ones(nn), units="g") + p.set_val("selector", np.array([0, 1, 2, 0, 2])) p.run_model() - assert_near_equal(p['result'], np.array([5.7, -1000., 42., 2., 42.])) - - partials = p.check_partials(method='cs',compact_print=True) + assert_near_equal(p["result"], np.array([5.7, -1000.0, 42.0, 2.0, 42.0])) + + partials = p.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - p.set_val('A', 5.*np.ones(nn), units='g') - p.set_val('B', 6.*np.ones(nn), units='g') - p.set_val('C', 7.*np.ones(nn), units='g') - p.set_val('selector', np.zeros(nn)) + p.set_val("A", 5.0 * np.ones(nn), units="g") + p.set_val("B", 6.0 * np.ones(nn), units="g") + p.set_val("C", 7.0 * np.ones(nn), units="g") + p.set_val("selector", np.zeros(nn)) p.run_model() - assert_near_equal(p['result'], 5.*np.ones(nn)) - - p.set_val('selector', np.ones(nn)) + assert_near_equal(p["result"], 5.0 * np.ones(nn)) + + p.set_val("selector", np.ones(nn)) p.run_model() - assert_near_equal(p['result'], 6.*np.ones(nn)) + assert_near_equal(p["result"], 6.0 * np.ones(nn)) - p.set_val('selector', 2.*np.ones(nn)) + p.set_val("selector", 2.0 * np.ones(nn)) p.run_model() - assert_near_equal(p['result'], 7.*np.ones(nn)) + assert_near_equal(p["result"], 7.0 * np.ones(nn)) - p.set_val('selector', np.array([-1, 1, 0, 2, 0])) + p.set_val("selector", np.array([-1, 1, 0, 2, 0])) with self.assertRaises(RuntimeWarning): p.run_model() - - p.set_val('selector', np.array([0, 1, -1, 2, 3])) + + p.set_val("selector", np.array([0, 1, -1, 2, 3])) with self.assertRaises(RuntimeWarning): - p.run_model() \ No newline at end of file + p.run_model() diff --git a/openconcept/utilities/visualization.py b/openconcept/utilities/visualization.py index 37d95592..c2ed10f1 100644 --- a/openconcept/utilities/visualization.py +++ b/openconcept/utilities/visualization.py @@ -5,17 +5,20 @@ pass import numpy as np -def plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, x_label=None, y_labels=None, marker='o', plot_title='Trajectory'): + +def plot_trajectory( + prob, x_var, x_unit, y_vars, y_units, phases, x_label=None, y_labels=None, marker="o", plot_title="Trajectory" +): val_list = [] for phase in phases: - val_list.append(prob.get_val(phase + '.' + x_var, units=x_unit)) + val_list.append(prob.get_val(phase + "." + x_var, units=x_unit)) x_vec = np.concatenate(val_list) for i, y_var in enumerate(y_vars): val_list = [] for phase in phases: - val_list.append(prob.get_val(phase + '.' + y_var, units=y_units[i])) + val_list.append(prob.get_val(phase + "." + y_var, units=y_units[i])) y_vec = np.concatenate(val_list) plt.figure() plt.plot(x_vec, y_vec, marker) @@ -31,7 +34,21 @@ def plot_trajectory(prob, x_var, x_unit, y_vars, y_units, phases, x_label=None, plt.title(plot_title) plt.show() -def plot_trajectory_grid(cases, x_var, x_unit, y_vars, y_units, phases, x_label=None, y_labels=None, grid_layout=[5,2], marker='o', savefig=None, figsize=None): + +def plot_trajectory_grid( + cases, + x_var, + x_unit, + y_vars, + y_units, + phases, + x_label=None, + y_labels=None, + grid_layout=[5, 2], + marker="o", + savefig=None, + figsize=None, +): """ Plots multiple trajectories against each other Cases is a list of OpenMDAO CaseReader cases which act like OpenMDAO problems @@ -40,7 +57,7 @@ def plot_trajectory_grid(cases, x_var, x_unit, y_vars, y_units, phases, x_label= for case in cases: val_list = [] for phase in phases: - val_list.append(case.get_val(phase + '.' + x_var, units=x_unit)) + val_list.append(case.get_val(phase + "." + x_var, units=x_unit)) x_vec = np.concatenate(val_list) x_vecs.append(x_vec) @@ -54,7 +71,7 @@ def plot_trajectory_grid(cases, x_var, x_unit, y_vars, y_units, phases, x_label= # write the file if savefig is not None: fig.tight_layout() - plt.savefig(savefig+'_'+str(file_counter)+'.pdf') + plt.savefig(savefig + "_" + str(file_counter) + ".pdf") fig, axs = plt.subplots(grid_layout[0], grid_layout[1], sharex=True, figsize=figsize) file_counter += 1 counter_within_file = 0 @@ -64,10 +81,10 @@ def plot_trajectory_grid(cases, x_var, x_unit, y_vars, y_units, phases, x_label= for j, case in enumerate(cases): val_list = [] for phase in phases: - val_list.append(case.get_val(phase + '.' + y_var, units=y_units[i])) + val_list.append(case.get_val(phase + "." + y_var, units=y_units[i])) y_vec = np.concatenate(val_list) axs[row_no, col_no].plot(x_vecs[j], y_vec, marker) - if row_no + 1 == grid_layout[0]: # last row + if row_no + 1 == grid_layout[0]: # last row if x_label is None: axs[row_no, col_no].set(xlabel=x_var) else: @@ -77,11 +94,9 @@ def plot_trajectory_grid(cases, x_var, x_unit, y_vars, y_units, phases, x_label= axs[row_no, col_no].set(ylabel=y_labels[i]) else: axs[row_no, col_no].set(y_var) - - counter_within_file += 1 + counter_within_file += 1 if savefig is not None: fig.tight_layout() - plt.savefig(savefig+'_'+str(file_counter)+'.pdf') - + plt.savefig(savefig + "_" + str(file_counter) + ".pdf") diff --git a/openconcept/weights/weights_turboprop.py b/openconcept/weights/weights_turboprop.py index 6cde9519..bc8a97af 100644 --- a/openconcept/weights/weights_turboprop.py +++ b/openconcept/weights/weights_turboprop.py @@ -6,66 +6,167 @@ ##TODO: add fuel system weight back in (depends on Wf, which depends on MTOW and We, and We depends on fuel system weight) + class WingWeight_SmallTurboprop(ExplicitComponent): """Inputs: MTOW, ac|geom|wing|S_ref, ac|geom|wing|AR, ac|geom|wing|c4sweep, ac|geom|wing|taper, ac|geom|wing|toverc, V_H (max SL speed) Outputs: W_wing Metadata: n_ult (ult load factor) """ - def initialize(self): - #self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - #define configuration parameters - self.options.declare('n_ult', default=3.8*1.5, desc='Ultimate load factor (dimensionless)') + def initialize(self): + # self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + # define configuration parameters + self.options.declare("n_ult", default=3.8 * 1.5, desc="Ultimate load factor (dimensionless)") def setup(self): - #nn = self.options['num_nodes'] - self.add_input('ac|weights|MTOW', units='lb', desc='Maximum rated takeoff weight') - self.add_input('ac|weights|W_fuel_max', units='lb', desc='Fuel weight') - self.add_input('ac|geom|wing|S_ref', units='ft**2', desc='Reference wing area in sq ft') - self.add_input('ac|geom|wing|AR', desc='Wing aspect ratio') - self.add_input('ac|geom|wing|c4sweep', units='rad', desc='Quarter-chord sweep angle') - self.add_input('ac|geom|wing|taper', desc='Wing taper ratio') - self.add_input('ac|geom|wing|toverc', desc='Wing max thickness to chord ratio') - #self.add_input('V_H', units='kn', desc='Max sea-level speed') - self.add_input('ac|q_cruise', units='lb*ft**-2') - - - #self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) - self.add_output('W_wing', units='lb', desc='Wing weight') - - self.declare_partials(['W_wing'], ['*']) + # nn = self.options['num_nodes'] + self.add_input("ac|weights|MTOW", units="lb", desc="Maximum rated takeoff weight") + self.add_input("ac|weights|W_fuel_max", units="lb", desc="Fuel weight") + self.add_input("ac|geom|wing|S_ref", units="ft**2", desc="Reference wing area in sq ft") + self.add_input("ac|geom|wing|AR", desc="Wing aspect ratio") + self.add_input("ac|geom|wing|c4sweep", units="rad", desc="Quarter-chord sweep angle") + self.add_input("ac|geom|wing|taper", desc="Wing taper ratio") + self.add_input("ac|geom|wing|toverc", desc="Wing max thickness to chord ratio") + # self.add_input('V_H', units='kn', desc='Max sea-level speed') + self.add_input("ac|q_cruise", units="lb*ft**-2") + + # self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) + self.add_output("W_wing", units="lb", desc="Wing weight") + + self.declare_partials(["W_wing"], ["*"]) def compute(self, inputs, outputs): - n_ult = self.options['n_ult'] - #USAF method, Roskam PVC5pg68eq5.4 - #W_wing_USAF = 96.948*((inputs['ac|weights|MTOW']*n_ult/1e5)**0.65 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep']))**0.57 * (inputs['ac|geom|wing|S_ref']/100)**0.61 * ((1+inputs['ac|geom|wing|taper'])/2/inputs['ac|geom|wing|toverc'])**0.36 * (1+inputs['V_H']/500)**0.5)**0.993 - #Torenbeek, Roskam PVC5p68eq5.5 - #b = math.sqrt(inputs['ac|geom|wing|S_ref']*inputs['ac|geom|wing|AR']) - #root_chord = 2*inputs['ac|geom|wing|S_ref']/b/(1+inputs['ac|geom|wing|taper']) - #tr = root_chord * inputs['ac|geom|wing|toverc'] - #c2sweep_wing = inputs['ac|geom|wing|c4sweep'] # a hack for now - #W_wing_Torenbeek = 0.00125*inputs['ac|weights|MTOW'] * (b/math.cos(c2sweep_wing))**0.75 * (1+ (6.3*math.cos(c2sweep_wing)/b)**0.5) * n_ult**0.55 * (b*inputs['ac|geom|wing|S_ref']/tr/inputs['ac|weights|MTOW']/math.cos(c2sweep_wing))**0.30 - - W_wing_Raymer = 0.036 * inputs['ac|geom|wing|S_ref']**0.758 * inputs['ac|weights|W_fuel_max']**0.0035 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**0.6 * inputs['ac|q_cruise']**0.006 * inputs['ac|geom|wing|taper']**0.04 * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**-0.3 * (n_ult * inputs['ac|weights|MTOW'])**0.49 - - outputs['W_wing'] = W_wing_Raymer + n_ult = self.options["n_ult"] + # USAF method, Roskam PVC5pg68eq5.4 + # W_wing_USAF = 96.948*((inputs['ac|weights|MTOW']*n_ult/1e5)**0.65 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep']))**0.57 * (inputs['ac|geom|wing|S_ref']/100)**0.61 * ((1+inputs['ac|geom|wing|taper'])/2/inputs['ac|geom|wing|toverc'])**0.36 * (1+inputs['V_H']/500)**0.5)**0.993 + # Torenbeek, Roskam PVC5p68eq5.5 + # b = math.sqrt(inputs['ac|geom|wing|S_ref']*inputs['ac|geom|wing|AR']) + # root_chord = 2*inputs['ac|geom|wing|S_ref']/b/(1+inputs['ac|geom|wing|taper']) + # tr = root_chord * inputs['ac|geom|wing|toverc'] + # c2sweep_wing = inputs['ac|geom|wing|c4sweep'] # a hack for now + # W_wing_Torenbeek = 0.00125*inputs['ac|weights|MTOW'] * (b/math.cos(c2sweep_wing))**0.75 * (1+ (6.3*math.cos(c2sweep_wing)/b)**0.5) * n_ult**0.55 * (b*inputs['ac|geom|wing|S_ref']/tr/inputs['ac|weights|MTOW']/math.cos(c2sweep_wing))**0.30 + + W_wing_Raymer = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** 0.758 + * inputs["ac|weights|W_fuel_max"] ** 0.0035 + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** 0.6 + * inputs["ac|q_cruise"] ** 0.006 + * inputs["ac|geom|wing|taper"] ** 0.04 + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** -0.3 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.49 + ) + + outputs["W_wing"] = W_wing_Raymer def compute_partials(self, inputs, J): - n_ult = self.options['n_ult'] - J['W_wing','ac|weights|MTOW'] = 0.036 * inputs['ac|geom|wing|S_ref']**0.758 * inputs['ac|weights|W_fuel_max']**0.0035 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**0.6 * inputs['ac|q_cruise']**0.006 * inputs['ac|geom|wing|taper']**0.04 * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**-0.3 * (n_ult * inputs['ac|weights|MTOW'])**(0.49-1)*n_ult*0.49 - J['W_wing','ac|weights|W_fuel_max'] = 0.036 * inputs['ac|geom|wing|S_ref']**0.758 * 0.0035 * inputs['ac|weights|W_fuel_max']**(0.0035-1) * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**0.6 * inputs['ac|q_cruise']**0.006 * inputs['ac|geom|wing|taper']**0.04 * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**-0.3 * (n_ult * inputs['ac|weights|MTOW'])**0.49 - J['W_wing','ac|geom|wing|S_ref'] = 0.036 * inputs['ac|geom|wing|S_ref']**(0.758-1)*0.758 * inputs['ac|weights|W_fuel_max']**0.0035 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**0.6 * inputs['ac|q_cruise']**0.006 * inputs['ac|geom|wing|taper']**0.04 * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**-0.3 * (n_ult * inputs['ac|weights|MTOW'])**0.49 - J['W_wing','ac|geom|wing|AR'] = 0.036 * inputs['ac|geom|wing|S_ref']**0.758 * inputs['ac|weights|W_fuel_max']**0.0035 * 0.6 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**(0.6-1) / math.cos(inputs['ac|geom|wing|c4sweep'])**2 * inputs['ac|q_cruise']**0.006 * inputs['ac|geom|wing|taper']**0.04 * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**-0.3 * (n_ult * inputs['ac|weights|MTOW'])**0.49 - c4const = 0.036 * inputs['ac|geom|wing|S_ref']**0.758 * inputs['ac|weights|W_fuel_max']**0.0035 * inputs['ac|q_cruise']**0.006 * inputs['ac|geom|wing|taper']**0.04 * (n_ult * inputs['ac|weights|MTOW'])**0.49 - c4multa = (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**0.6 - c4multb = (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**-0.3 - dc4multa = 0.6 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**(0.6-1) * (-2* inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**3) * (-math.sin(inputs['ac|geom|wing|c4sweep'])) - dc4multb = -0.3 * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**(-0.3-1) * -100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep'])**2 * (-math.sin(inputs['ac|geom|wing|c4sweep'])) - J['W_wing','ac|geom|wing|c4sweep'] = c4const*(c4multa*dc4multb + c4multb*dc4multa) - J['W_wing','ac|geom|wing|taper'] = 0.036 * inputs['ac|geom|wing|S_ref']**0.758 * inputs['ac|weights|W_fuel_max']**0.0035 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**0.6 * inputs['ac|q_cruise']**0.006 * 0.04 * inputs['ac|geom|wing|taper']**(0.04-1) * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**-0.3 * (n_ult * inputs['ac|weights|MTOW'])**0.49 - J['W_wing','ac|geom|wing|toverc'] = 0.036 * inputs['ac|geom|wing|S_ref']**0.758 * inputs['ac|weights|W_fuel_max']**0.0035 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**0.6 * inputs['ac|q_cruise']**0.006 * inputs['ac|geom|wing|taper']**0.04 * -0.3 * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**(-0.3-1) * (100/math.cos(inputs['ac|geom|wing|c4sweep'])) * (n_ult * inputs['ac|weights|MTOW'])**0.49 - J['W_wing','ac|q_cruise'] = 0.036 * inputs['ac|geom|wing|S_ref']**0.758 * inputs['ac|weights|W_fuel_max']**0.0035 * (inputs['ac|geom|wing|AR']/math.cos(inputs['ac|geom|wing|c4sweep'])**2)**0.6 * 0.006 * inputs['ac|q_cruise']**(0.006-1) * inputs['ac|geom|wing|taper']**0.04 * (100*inputs['ac|geom|wing|toverc']/math.cos(inputs['ac|geom|wing|c4sweep']))**-0.3 * (n_ult * inputs['ac|weights|MTOW'])**0.49 + n_ult = self.options["n_ult"] + J["W_wing", "ac|weights|MTOW"] = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** 0.758 + * inputs["ac|weights|W_fuel_max"] ** 0.0035 + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** 0.6 + * inputs["ac|q_cruise"] ** 0.006 + * inputs["ac|geom|wing|taper"] ** 0.04 + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** -0.3 + * (n_ult * inputs["ac|weights|MTOW"]) ** (0.49 - 1) + * n_ult + * 0.49 + ) + J["W_wing", "ac|weights|W_fuel_max"] = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** 0.758 + * 0.0035 + * inputs["ac|weights|W_fuel_max"] ** (0.0035 - 1) + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** 0.6 + * inputs["ac|q_cruise"] ** 0.006 + * inputs["ac|geom|wing|taper"] ** 0.04 + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** -0.3 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.49 + ) + J["W_wing", "ac|geom|wing|S_ref"] = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** (0.758 - 1) + * 0.758 + * inputs["ac|weights|W_fuel_max"] ** 0.0035 + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** 0.6 + * inputs["ac|q_cruise"] ** 0.006 + * inputs["ac|geom|wing|taper"] ** 0.04 + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** -0.3 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.49 + ) + J["W_wing", "ac|geom|wing|AR"] = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** 0.758 + * inputs["ac|weights|W_fuel_max"] ** 0.0035 + * 0.6 + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** (0.6 - 1) + / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2 + * inputs["ac|q_cruise"] ** 0.006 + * inputs["ac|geom|wing|taper"] ** 0.04 + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** -0.3 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.49 + ) + c4const = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** 0.758 + * inputs["ac|weights|W_fuel_max"] ** 0.0035 + * inputs["ac|q_cruise"] ** 0.006 + * inputs["ac|geom|wing|taper"] ** 0.04 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.49 + ) + c4multa = (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** 0.6 + c4multb = (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** -0.3 + dc4multa = ( + 0.6 + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** (0.6 - 1) + * (-2 * inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 3) + * (-math.sin(inputs["ac|geom|wing|c4sweep"])) + ) + dc4multb = ( + -0.3 + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** (-0.3 - 1) + * -100 + * inputs["ac|geom|wing|toverc"] + / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2 + * (-math.sin(inputs["ac|geom|wing|c4sweep"])) + ) + J["W_wing", "ac|geom|wing|c4sweep"] = c4const * (c4multa * dc4multb + c4multb * dc4multa) + J["W_wing", "ac|geom|wing|taper"] = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** 0.758 + * inputs["ac|weights|W_fuel_max"] ** 0.0035 + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** 0.6 + * inputs["ac|q_cruise"] ** 0.006 + * 0.04 + * inputs["ac|geom|wing|taper"] ** (0.04 - 1) + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** -0.3 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.49 + ) + J["W_wing", "ac|geom|wing|toverc"] = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** 0.758 + * inputs["ac|weights|W_fuel_max"] ** 0.0035 + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** 0.6 + * inputs["ac|q_cruise"] ** 0.006 + * inputs["ac|geom|wing|taper"] ** 0.04 + * -0.3 + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** (-0.3 - 1) + * (100 / math.cos(inputs["ac|geom|wing|c4sweep"])) + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.49 + ) + J["W_wing", "ac|q_cruise"] = ( + 0.036 + * inputs["ac|geom|wing|S_ref"] ** 0.758 + * inputs["ac|weights|W_fuel_max"] ** 0.0035 + * (inputs["ac|geom|wing|AR"] / math.cos(inputs["ac|geom|wing|c4sweep"]) ** 2) ** 0.6 + * 0.006 + * inputs["ac|q_cruise"] ** (0.006 - 1) + * inputs["ac|geom|wing|taper"] ** 0.04 + * (100 * inputs["ac|geom|wing|toverc"] / math.cos(inputs["ac|geom|wing|c4sweep"])) ** -0.3 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.49 + ) class EmpennageWeight_SmallTurboprop(ExplicitComponent): @@ -74,27 +175,27 @@ class EmpennageWeight_SmallTurboprop(ExplicitComponent): Metadata: n_ult (ult load factor) """ - def initialize(self): - self.options.declare('n_ult', default=3.8*1.5, desc='Ultimate load factor (dimensionless)') + def initialize(self): + self.options.declare("n_ult", default=3.8 * 1.5, desc="Ultimate load factor (dimensionless)") def setup(self): - self.add_input('ac|geom|hstab|S_ref', units='ft**2', desc='Projected horiz stab area in sq ft') - self.add_input('ac|geom|vstab|S_ref', units='ft**2', desc='Projected vert stab area in sq ft') - #self.add_input('ac|geom|hstab|c4_to_wing_c4', units='ft', desc='Distance from wing c/4 to horiz stab c/4 (tail arm distance)') + self.add_input("ac|geom|hstab|S_ref", units="ft**2", desc="Projected horiz stab area in sq ft") + self.add_input("ac|geom|vstab|S_ref", units="ft**2", desc="Projected vert stab area in sq ft") + # self.add_input('ac|geom|hstab|c4_to_wing_c4', units='ft', desc='Distance from wing c/4 to horiz stab c/4 (tail arm distance)') # self.add_input('ac|weights|MTOW', units='lb', desc='Maximum rated takeoff weight') # self.add_input('AR_h', desc='Horiz stab aspect ratio') # self.add_input('AR_v', units='rad', desc='Vert stab aspect ratio') # self.add_input('troot_h', units='ft', desc='Horiz stab root thickness (ft)') # self.add_input('troot_v', units='ft', desc='Vert stab root thickness (ft)') - #self.add_input('ac|q_cruise', units='lb*ft**-2', desc='Cruise dynamic pressure') + # self.add_input('ac|q_cruise', units='lb*ft**-2', desc='Cruise dynamic pressure') - self.add_output('W_empennage', units='lb', desc='Empennage weight') - self.declare_partials(['W_empennage'], ['*']) + self.add_output("W_empennage", units="lb", desc="Empennage weight") + self.declare_partials(["W_empennage"], ["*"]) def compute(self, inputs, outputs): - n_ult = self.options['n_ult'] - #USAF method, Roskam PVC5pg72eq5.14/15 + n_ult = self.options["n_ult"] + # USAF method, Roskam PVC5pg72eq5.14/15 # bh = math.sqrt(inputs['ac|geom|hstab|S_ref']*inputs['AR_h']) # bv = math.sqrt(inputs['ac|geom|vstab|S_ref']*inputs['AR_v']) # # Wh = 127 * ((inputs['ac|weights|MTOW']*n_ult/1e5)**0.87 * (inputs['ac|geom|hstab|S_ref']/100)**1.2 * 0.289*(inputs['ac|geom|hstab|c4_to_wing_c4']/10)**0.483 * (bh/inputs['troot_h'])**0.5)**0.458 @@ -103,59 +204,176 @@ def compute(self, inputs, outputs): # # Wemp_USAF = Wh + Wv - #Torenbeek, Roskam PVC5p73eq5.16 - Wemp_Torenbeek = 0.04 * (n_ult * (inputs['ac|geom|vstab|S_ref'] + inputs['ac|geom|hstab|S_ref'])**2)**0.75 - outputs['W_empennage'] = Wemp_Torenbeek + # Torenbeek, Roskam PVC5p73eq5.16 + Wemp_Torenbeek = 0.04 * (n_ult * (inputs["ac|geom|vstab|S_ref"] + inputs["ac|geom|hstab|S_ref"]) ** 2) ** 0.75 + outputs["W_empennage"] = Wemp_Torenbeek def compute_partials(self, inputs, J): - n_ult = self.options['n_ult'] - J['W_empennage','ac|geom|vstab|S_ref'] = 0.75* 0.04 * (n_ult * (inputs['ac|geom|vstab|S_ref'] + inputs['ac|geom|hstab|S_ref'])**2)**(0.75-1)*(n_ult * 2* (inputs['ac|geom|vstab|S_ref'] + inputs['ac|geom|hstab|S_ref'])) - J['W_empennage','ac|geom|hstab|S_ref'] = 0.75* 0.04 * (n_ult * (inputs['ac|geom|vstab|S_ref'] + inputs['ac|geom|hstab|S_ref'])**2)**(0.75-1)*(n_ult * 2* (inputs['ac|geom|vstab|S_ref'] + inputs['ac|geom|hstab|S_ref'])) + n_ult = self.options["n_ult"] + J["W_empennage", "ac|geom|vstab|S_ref"] = ( + 0.75 + * 0.04 + * (n_ult * (inputs["ac|geom|vstab|S_ref"] + inputs["ac|geom|hstab|S_ref"]) ** 2) ** (0.75 - 1) + * (n_ult * 2 * (inputs["ac|geom|vstab|S_ref"] + inputs["ac|geom|hstab|S_ref"])) + ) + J["W_empennage", "ac|geom|hstab|S_ref"] = ( + 0.75 + * 0.04 + * (n_ult * (inputs["ac|geom|vstab|S_ref"] + inputs["ac|geom|hstab|S_ref"]) ** 2) ** (0.75 - 1) + * (n_ult * 2 * (inputs["ac|geom|vstab|S_ref"] + inputs["ac|geom|hstab|S_ref"])) + ) class FuselageWeight_SmallTurboprop(ExplicitComponent): def initialize(self): - #self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - #define configuration parameters - self.options.declare('n_ult', default=3.8*1.5, desc='Ultimate load factor (dimensionless)') - + # self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + # define configuration parameters + self.options.declare("n_ult", default=3.8 * 1.5, desc="Ultimate load factor (dimensionless)") def setup(self): - #nn = self.options['num_nodes'] - self.add_input('ac|weights|MTOW', units='lb', desc='Maximum rated takeoff weight') - self.add_input('ac|geom|fuselage|length', units='ft', desc='Fuselage length (not counting nacelle') - self.add_input('ac|geom|fuselage|height', units='ft', desc='Fuselage height') - self.add_input('ac|geom|fuselage|width', units='ft', desc='Fuselage weidth') - #self.add_input('V_C', units='kn', desc='Indicated cruise airspeed (KEAS)') - #self.add_input('V_MO', units='kn', desc='Max operating speed (indicated)') - self.add_input('ac|geom|fuselage|S_wet', units='ft**2', desc='Fuselage shell area') - self.add_input('ac|geom|hstab|c4_to_wing_c4', units='ft', desc='Horiz tail arm') - self.add_input('ac|q_cruise', units='lb*ft**-2', desc='Dynamic pressure at cruise') - - #self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) - self.add_output('W_fuselage', units='lb', desc='Fuselage weight') - self.declare_partials(['W_fuselage'], ['*']) + # nn = self.options['num_nodes'] + self.add_input("ac|weights|MTOW", units="lb", desc="Maximum rated takeoff weight") + self.add_input("ac|geom|fuselage|length", units="ft", desc="Fuselage length (not counting nacelle") + self.add_input("ac|geom|fuselage|height", units="ft", desc="Fuselage height") + self.add_input("ac|geom|fuselage|width", units="ft", desc="Fuselage weidth") + # self.add_input('V_C', units='kn', desc='Indicated cruise airspeed (KEAS)') + # self.add_input('V_MO', units='kn', desc='Max operating speed (indicated)') + self.add_input("ac|geom|fuselage|S_wet", units="ft**2", desc="Fuselage shell area") + self.add_input("ac|geom|hstab|c4_to_wing_c4", units="ft", desc="Horiz tail arm") + self.add_input("ac|q_cruise", units="lb*ft**-2", desc="Dynamic pressure at cruise") + + # self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) + self.add_output("W_fuselage", units="lb", desc="Fuselage weight") + self.declare_partials(["W_fuselage"], ["*"]) def compute(self, inputs, outputs): - n_ult = self.options['n_ult'] - #USAF method, Roskam PVC5pg76eq5.25 + n_ult = self.options["n_ult"] + # USAF method, Roskam PVC5pg76eq5.25 # W_fuselage_USAF = 200*((inputs['ac|weights|MTOW']*n_ult/1e5)**0.286 * (inputs['ac|geom|fuselage|length']/10)**0.857 * (inputs['ac|geom|fuselage|width']+inputs['ac|geom|fuselage|height'])/10 * (inputs['V_C']/100)**0.338)**1.1 # print(W_fuselage_USAF) - #W_fuselage_Torenbeek = 0.021 * 1.08 * ((inputs['V_MO']*inputs['ac|geom|hstab|c4_to_wing_c4']/(inputs['ac|geom|fuselage|width']+inputs['ac|geom|fuselage|height']))**0.5 * inputs['ac|geom|fuselage|S_wet']**1.2) - W_press = 11.9*(math.pi*(inputs['ac|geom|fuselage|width']+inputs['ac|geom|fuselage|height'])/2*inputs['ac|geom|fuselage|length']*0.8 * 8)**0.271 - W_fuselage_Raymer = 0.052 * inputs['ac|geom|fuselage|S_wet']**1.086 * (n_ult * inputs['ac|weights|MTOW'])**0.177 * inputs['ac|geom|hstab|c4_to_wing_c4']**-0.051 * (inputs['ac|geom|fuselage|length']/inputs['ac|geom|fuselage|height'])**-0.072 * inputs['ac|q_cruise']**0.241 + W_press - outputs['W_fuselage'] = W_fuselage_Raymer + # W_fuselage_Torenbeek = 0.021 * 1.08 * ((inputs['V_MO']*inputs['ac|geom|hstab|c4_to_wing_c4']/(inputs['ac|geom|fuselage|width']+inputs['ac|geom|fuselage|height']))**0.5 * inputs['ac|geom|fuselage|S_wet']**1.2) + W_press = ( + 11.9 + * ( + math.pi + * (inputs["ac|geom|fuselage|width"] + inputs["ac|geom|fuselage|height"]) + / 2 + * inputs["ac|geom|fuselage|length"] + * 0.8 + * 8 + ) + ** 0.271 + ) + W_fuselage_Raymer = ( + 0.052 + * inputs["ac|geom|fuselage|S_wet"] ** 1.086 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.177 + * inputs["ac|geom|hstab|c4_to_wing_c4"] ** -0.051 + * (inputs["ac|geom|fuselage|length"] / inputs["ac|geom|fuselage|height"]) ** -0.072 + * inputs["ac|q_cruise"] ** 0.241 + + W_press + ) + outputs["W_fuselage"] = W_fuselage_Raymer def compute_partials(self, inputs, J): - n_ult = self.options['n_ult'] - J['W_fuselage','ac|weights|MTOW'] = 0.052 * inputs['ac|geom|fuselage|S_wet']**1.086 * 0.177 * n_ult * (n_ult * inputs['ac|weights|MTOW'])**(0.177-1) * inputs['ac|geom|hstab|c4_to_wing_c4']**-0.051 * (inputs['ac|geom|fuselage|length']/inputs['ac|geom|fuselage|height'])**-0.072 * inputs['ac|q_cruise']**0.241 - J['W_fuselage','ac|geom|fuselage|width'] = 0.271 * 11.9*(math.pi*(inputs['ac|geom|fuselage|width']+inputs['ac|geom|fuselage|height'])/2*inputs['ac|geom|fuselage|length']*0.8 * 8)**(0.271-1) * (math.pi/2*inputs['ac|geom|fuselage|length']*0.8 * 8) - J['W_fuselage','ac|geom|fuselage|height'] = 0.271 * 11.9*(math.pi*(inputs['ac|geom|fuselage|width']+inputs['ac|geom|fuselage|height'])/2*inputs['ac|geom|fuselage|length']*0.8 * 8)**(0.271-1) * (math.pi/2*inputs['ac|geom|fuselage|length']*0.8 * 8) + 0.052 * inputs['ac|geom|fuselage|S_wet']**1.086 * (n_ult * inputs['ac|weights|MTOW'])**0.177 * inputs['ac|geom|hstab|c4_to_wing_c4']**-0.051 * -0.072 * (inputs['ac|geom|fuselage|length']/inputs['ac|geom|fuselage|height'])**(-0.072-1) * (-inputs['ac|geom|fuselage|length']/inputs['ac|geom|fuselage|height']**2) * inputs['ac|q_cruise']**0.241 - J['W_fuselage','ac|geom|fuselage|length'] = 0.271 * 11.9*(math.pi*(inputs['ac|geom|fuselage|width']+inputs['ac|geom|fuselage|height'])/2*inputs['ac|geom|fuselage|length']*0.8 * 8)**(0.271-1) * (math.pi*(inputs['ac|geom|fuselage|width']+inputs['ac|geom|fuselage|height'])/2*0.8 * 8) + 0.052 * inputs['ac|geom|fuselage|S_wet']**1.086 * (n_ult * inputs['ac|weights|MTOW'])**0.177 * inputs['ac|geom|hstab|c4_to_wing_c4']**-0.051 * -0.072 * (inputs['ac|geom|fuselage|length']/inputs['ac|geom|fuselage|height'])**(-0.072-1) * (1/inputs['ac|geom|fuselage|height']) * inputs['ac|q_cruise']**0.241 - J['W_fuselage','ac|geom|fuselage|S_wet'] = 0.052 * 1.086 * inputs['ac|geom|fuselage|S_wet']**(1.086-1) * (n_ult * inputs['ac|weights|MTOW'])**0.177 * inputs['ac|geom|hstab|c4_to_wing_c4']**-0.051 * (inputs['ac|geom|fuselage|length']/inputs['ac|geom|fuselage|height'])**-0.072 * inputs['ac|q_cruise']**0.241 - J['W_fuselage','ac|geom|hstab|c4_to_wing_c4'] = 0.052 * inputs['ac|geom|fuselage|S_wet']**1.086 * (n_ult * inputs['ac|weights|MTOW'])**0.177 * -0.051 * inputs['ac|geom|hstab|c4_to_wing_c4']**(-0.051-1) * (inputs['ac|geom|fuselage|length']/inputs['ac|geom|fuselage|height'])**-0.072 * inputs['ac|q_cruise']**0.241 - J['W_fuselage','ac|q_cruise'] = 0.052 * inputs['ac|geom|fuselage|S_wet']**1.086 * (n_ult * inputs['ac|weights|MTOW'])**0.177 * inputs['ac|geom|hstab|c4_to_wing_c4']**-0.051 * (inputs['ac|geom|fuselage|length']/inputs['ac|geom|fuselage|height'])**-0.072 * 0.241 * inputs['ac|q_cruise']**(0.241-1) + n_ult = self.options["n_ult"] + J["W_fuselage", "ac|weights|MTOW"] = ( + 0.052 + * inputs["ac|geom|fuselage|S_wet"] ** 1.086 + * 0.177 + * n_ult + * (n_ult * inputs["ac|weights|MTOW"]) ** (0.177 - 1) + * inputs["ac|geom|hstab|c4_to_wing_c4"] ** -0.051 + * (inputs["ac|geom|fuselage|length"] / inputs["ac|geom|fuselage|height"]) ** -0.072 + * inputs["ac|q_cruise"] ** 0.241 + ) + J["W_fuselage", "ac|geom|fuselage|width"] = ( + 0.271 + * 11.9 + * ( + math.pi + * (inputs["ac|geom|fuselage|width"] + inputs["ac|geom|fuselage|height"]) + / 2 + * inputs["ac|geom|fuselage|length"] + * 0.8 + * 8 + ) + ** (0.271 - 1) + * (math.pi / 2 * inputs["ac|geom|fuselage|length"] * 0.8 * 8) + ) + J["W_fuselage", "ac|geom|fuselage|height"] = ( + 0.271 + * 11.9 + * ( + math.pi + * (inputs["ac|geom|fuselage|width"] + inputs["ac|geom|fuselage|height"]) + / 2 + * inputs["ac|geom|fuselage|length"] + * 0.8 + * 8 + ) + ** (0.271 - 1) + * (math.pi / 2 * inputs["ac|geom|fuselage|length"] * 0.8 * 8) + + 0.052 + * inputs["ac|geom|fuselage|S_wet"] ** 1.086 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.177 + * inputs["ac|geom|hstab|c4_to_wing_c4"] ** -0.051 + * -0.072 + * (inputs["ac|geom|fuselage|length"] / inputs["ac|geom|fuselage|height"]) ** (-0.072 - 1) + * (-inputs["ac|geom|fuselage|length"] / inputs["ac|geom|fuselage|height"] ** 2) + * inputs["ac|q_cruise"] ** 0.241 + ) + J["W_fuselage", "ac|geom|fuselage|length"] = ( + 0.271 + * 11.9 + * ( + math.pi + * (inputs["ac|geom|fuselage|width"] + inputs["ac|geom|fuselage|height"]) + / 2 + * inputs["ac|geom|fuselage|length"] + * 0.8 + * 8 + ) + ** (0.271 - 1) + * (math.pi * (inputs["ac|geom|fuselage|width"] + inputs["ac|geom|fuselage|height"]) / 2 * 0.8 * 8) + + 0.052 + * inputs["ac|geom|fuselage|S_wet"] ** 1.086 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.177 + * inputs["ac|geom|hstab|c4_to_wing_c4"] ** -0.051 + * -0.072 + * (inputs["ac|geom|fuselage|length"] / inputs["ac|geom|fuselage|height"]) ** (-0.072 - 1) + * (1 / inputs["ac|geom|fuselage|height"]) + * inputs["ac|q_cruise"] ** 0.241 + ) + J["W_fuselage", "ac|geom|fuselage|S_wet"] = ( + 0.052 + * 1.086 + * inputs["ac|geom|fuselage|S_wet"] ** (1.086 - 1) + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.177 + * inputs["ac|geom|hstab|c4_to_wing_c4"] ** -0.051 + * (inputs["ac|geom|fuselage|length"] / inputs["ac|geom|fuselage|height"]) ** -0.072 + * inputs["ac|q_cruise"] ** 0.241 + ) + J["W_fuselage", "ac|geom|hstab|c4_to_wing_c4"] = ( + 0.052 + * inputs["ac|geom|fuselage|S_wet"] ** 1.086 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.177 + * -0.051 + * inputs["ac|geom|hstab|c4_to_wing_c4"] ** (-0.051 - 1) + * (inputs["ac|geom|fuselage|length"] / inputs["ac|geom|fuselage|height"]) ** -0.072 + * inputs["ac|q_cruise"] ** 0.241 + ) + J["W_fuselage", "ac|q_cruise"] = ( + 0.052 + * inputs["ac|geom|fuselage|S_wet"] ** 1.086 + * (n_ult * inputs["ac|weights|MTOW"]) ** 0.177 + * inputs["ac|geom|hstab|c4_to_wing_c4"] ** -0.051 + * (inputs["ac|geom|fuselage|length"] / inputs["ac|geom|fuselage|height"]) ** -0.072 + * 0.241 + * inputs["ac|q_cruise"] ** (0.241 - 1) + ) + class NacelleWeight_SmallSingleTurboprop(ExplicitComponent): """Inputs: MTOW, ac|geom|wing|S_ref, ac|geom|wing|AR, ac|geom|wing|c4sweep, ac|geom|wing|taper, ac|geom|wing|toverc, V_H (max SL speed) @@ -163,30 +381,31 @@ class NacelleWeight_SmallSingleTurboprop(ExplicitComponent): Metadata: n_ult (ult load factor) """ - def initialize(self): - #self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - #define configuration parameters - self.options.declare('n_ult', default=3.8*1.5, desc='Ultimate load factor (dimensionless)') + def initialize(self): + # self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + # define configuration parameters + self.options.declare("n_ult", default=3.8 * 1.5, desc="Ultimate load factor (dimensionless)") def setup(self): - #nn = self.options['num_nodes'] - self.add_input('P_TO', units='hp', desc='Takeoff power') + # nn = self.options['num_nodes'] + self.add_input("P_TO", units="hp", desc="Takeoff power") - #self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) - self.add_output('W_nacelle', units='lb', desc='Nacelle weight') - self.declare_partials(['W_nacelle'], ['*']) + # self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) + self.add_output("W_nacelle", units="lb", desc="Nacelle weight") + self.declare_partials(["W_nacelle"], ["*"]) def compute(self, inputs, outputs): - n_ult = self.options['n_ult'] - #Torenbeek method, Roskam PVC5pg78eq5.30 - W_nacelle = 2.5*inputs['P_TO']**0.5 - outputs['W_nacelle'] = W_nacelle + n_ult = self.options["n_ult"] + # Torenbeek method, Roskam PVC5pg78eq5.30 + W_nacelle = 2.5 * inputs["P_TO"] ** 0.5 + outputs["W_nacelle"] = W_nacelle def compute_partials(self, inputs, J): - n_ult = self.options['n_ult'] - #Torenbeek method, Roskam PVC5pg78eq5.30 - J['W_nacelle','P_TO'] = 0.5 * 2.5*inputs['P_TO']**(0.5-1) + n_ult = self.options["n_ult"] + # Torenbeek method, Roskam PVC5pg78eq5.30 + J["W_nacelle", "P_TO"] = 0.5 * 2.5 * inputs["P_TO"] ** (0.5 - 1) + class NacelleWeight_MultiTurboprop(ExplicitComponent): """Inputs: MTOW, ac|geom|wing|S_ref, ac|geom|wing|AR, ac|geom|wing|c4sweep, ac|geom|wing|taper, ac|geom|wing|toverc, V_H (max SL speed) @@ -194,30 +413,31 @@ class NacelleWeight_MultiTurboprop(ExplicitComponent): Metadata: n_ult (ult load factor) """ - def initialize(self): - #self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - #define configuration parameters - self.options.declare('n_ult', default=3.8*1.5, desc='Ultimate load factor (dimensionless)') + def initialize(self): + # self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + # define configuration parameters + self.options.declare("n_ult", default=3.8 * 1.5, desc="Ultimate load factor (dimensionless)") def setup(self): - #nn = self.options['num_nodes'] - self.add_input('P_TO', units='hp', desc='Takeoff power') + # nn = self.options['num_nodes'] + self.add_input("P_TO", units="hp", desc="Takeoff power") - #self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) - self.add_output('W_nacelle', units='lb', desc='Nacelle weight') - self.declare_partials(['W_nacelle'], ['*']) + # self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) + self.add_output("W_nacelle", units="lb", desc="Nacelle weight") + self.declare_partials(["W_nacelle"], ["*"]) def compute(self, inputs, outputs): - n_ult = self.options['n_ult'] - #Torenbeek method, Roskam PVC5pg78eq5.33 - W_nacelle = 0.14*inputs['P_TO'] - outputs['W_nacelle'] = W_nacelle + n_ult = self.options["n_ult"] + # Torenbeek method, Roskam PVC5pg78eq5.33 + W_nacelle = 0.14 * inputs["P_TO"] + outputs["W_nacelle"] = W_nacelle def compute_partials(self, inputs, J): - n_ult = self.options['n_ult'] - #Torenbeek method, Roskam PVC5pg78eq5.30 - J['W_nacelle','P_TO'] = 0.14 + n_ult = self.options["n_ult"] + # Torenbeek method, Roskam PVC5pg78eq5.30 + J["W_nacelle", "P_TO"] = 0.14 + class LandingGearWeight_SmallTurboprop(ExplicitComponent): """Inputs: MTOW, ac|geom|wing|S_ref, ac|geom|wing|AR, ac|geom|wing|c4sweep, ac|geom|wing|taper, ac|geom|wing|toverc, V_H (max SL speed) @@ -225,178 +445,293 @@ class LandingGearWeight_SmallTurboprop(ExplicitComponent): Metadata: n_ult (ult load factor) """ - def initialize(self): - #self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - #define configuration parameters - self.options.declare('n_ult', default=3.8*1.5, desc='Ultimate load factor (dimensionless)') + def initialize(self): + # self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + # define configuration parameters + self.options.declare("n_ult", default=3.8 * 1.5, desc="Ultimate load factor (dimensionless)") def setup(self): - #nn = self.options['num_nodes'] + # nn = self.options['num_nodes'] # self.add_input('ac|weights|MTOW', units='lb',desc='Max takeoff weight') - self.add_input('ac|weights|MLW', units='lb', desc='Max landing weight') - self.add_input('ac|geom|maingear|length', units='ft', desc='Main landing gear extended length') - self.add_input('ac|geom|nosegear|length', units='ft', desc='Nose gear extended length') + self.add_input("ac|weights|MLW", units="lb", desc="Max landing weight") + self.add_input("ac|geom|maingear|length", units="ft", desc="Main landing gear extended length") + self.add_input("ac|geom|nosegear|length", units="ft", desc="Nose gear extended length") - #self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) - self.add_output('W_gear', units='lb', desc='Gear weight (nose and main)') - self.declare_partials(['W_gear'], ['*']) + # self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) + self.add_output("W_gear", units="lb", desc="Gear weight (nose and main)") + self.declare_partials(["W_gear"], ["*"]) def compute(self, inputs, outputs): - n_ult = self.options['n_ult'] - #Torenbeek method, Roskam PVC5pg82eq5.42 + n_ult = self.options["n_ult"] + # Torenbeek method, Roskam PVC5pg82eq5.42 # W_gear_Torenbeek_main = 33.0+0.04*inputs['ac|weights|MTOW']**0.75 + 0.021*inputs['ac|weights|MTOW'] # W_gear_Torenbeek_nose = 12.0+0.06*inputs['ac|weights|MTOW']**0.75 - W_gear_Raymer_main = 0.095*(n_ult*inputs['ac|weights|MLW'])**0.768 * (inputs['ac|geom|maingear|length']/12)**0.409 - W_gear_Raymer_nose = 0.125*(n_ult*inputs['ac|weights|MLW'])**0.566 * (inputs['ac|geom|nosegear|length']/12)**0.845 - + W_gear_Raymer_main = ( + 0.095 * (n_ult * inputs["ac|weights|MLW"]) ** 0.768 * (inputs["ac|geom|maingear|length"] / 12) ** 0.409 + ) + W_gear_Raymer_nose = ( + 0.125 * (n_ult * inputs["ac|weights|MLW"]) ** 0.566 * (inputs["ac|geom|nosegear|length"] / 12) ** 0.845 + ) W_gear = W_gear_Raymer_main + W_gear_Raymer_nose - outputs['W_gear'] = W_gear + outputs["W_gear"] = W_gear def compute_partials(self, inputs, J): - n_ult = self.options['n_ult'] - J['W_gear','ac|weights|MLW'] = 0.095*(n_ult*inputs['ac|weights|MLW'])**(0.768-1) * 0.768 * n_ult * (inputs['ac|geom|maingear|length']/12)**0.409 + 0.125*(n_ult*inputs['ac|weights|MLW'])**(0.566-1) * 0.566 * n_ult * (inputs['ac|geom|nosegear|length']/12)**0.845 - J['W_gear','ac|geom|maingear|length'] = 0.095*(n_ult*inputs['ac|weights|MLW'])**0.768 * (inputs['ac|geom|maingear|length']/12)**(0.409-1)* ( 1/12 ) * 0.409 - J['W_gear','ac|geom|nosegear|length'] = 0.125*(n_ult*inputs['ac|weights|MLW'])**0.566 * (inputs['ac|geom|nosegear|length']/12)**(0.845-1)*(1/12) *0.845 + n_ult = self.options["n_ult"] + J["W_gear", "ac|weights|MLW"] = ( + 0.095 + * (n_ult * inputs["ac|weights|MLW"]) ** (0.768 - 1) + * 0.768 + * n_ult + * (inputs["ac|geom|maingear|length"] / 12) ** 0.409 + + 0.125 + * (n_ult * inputs["ac|weights|MLW"]) ** (0.566 - 1) + * 0.566 + * n_ult + * (inputs["ac|geom|nosegear|length"] / 12) ** 0.845 + ) + J["W_gear", "ac|geom|maingear|length"] = ( + 0.095 + * (n_ult * inputs["ac|weights|MLW"]) ** 0.768 + * (inputs["ac|geom|maingear|length"] / 12) ** (0.409 - 1) + * (1 / 12) + * 0.409 + ) + J["W_gear", "ac|geom|nosegear|length"] = ( + 0.125 + * (n_ult * inputs["ac|weights|MLW"]) ** 0.566 + * (inputs["ac|geom|nosegear|length"] / 12) ** (0.845 - 1) + * (1 / 12) + * 0.845 + ) + class FuelSystemWeight_SmallTurboprop(ExplicitComponent): def initialize(self): - #self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') - #define configuration parameters - self.options.declare('Kfsp', default=6.55, desc='Fuel density (lbs/gal)') + # self.options.declare('num_nodes', default=1, desc='Number of flight/control conditions') + # define configuration parameters + self.options.declare("Kfsp", default=6.55, desc="Fuel density (lbs/gal)") # self.options.declare('num_tanks', default=2, desc='Number of fuel tanks') # self.options.declare('num_engines', default=1, desc='Number of engines') def setup(self): - #nn = self.options['num_nodes'] - self.add_input('ac|weights|W_fuel_max', units='lb',desc='Full fuel weight') + # nn = self.options['num_nodes'] + self.add_input("ac|weights|W_fuel_max", units="lb", desc="Full fuel weight") - #self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) - self.add_output('W_fuelsystem', units='lb', desc='Fuel system weight') - self.declare_partials('W_fuelsystem','ac|weights|W_fuel_max') + # self.add_output('heat_out', units='W', desc='Waste heat out',shape=(nn,)) + self.add_output("W_fuelsystem", units="lb", desc="Fuel system weight") + self.declare_partials("W_fuelsystem", "ac|weights|W_fuel_max") def compute(self, inputs, outputs): # n_t = self.options['num_tanks'] # n_e = self.options['num_engines'] - Kfsp = self.options['Kfsp'] - #Torenbeek method, Roskam PVC6pg92eq6.24 - #W_fs_Cessna = 0.4 * inputs['ac|weights|W_fuel_max'] / Kfsp + Kfsp = self.options["Kfsp"] + # Torenbeek method, Roskam PVC6pg92eq6.24 + # W_fs_Cessna = 0.4 * inputs['ac|weights|W_fuel_max'] / Kfsp # W_fs_Torenbeek = 80*(n_e+n_t-1) + 15*n_t**0.5 * (inputs['ac|weights|W_fuel_max']/Kfsp)**0.333 # print(W_fs_Torenbeek) # W_fs_USAF = 2.49* ((inputs['ac|weights|W_fuel_max']/Kfsp)**0.6 * n_t**0.20 * n_e**0.13)**1.21 # print(W_fs_USAF) - W_fs_Raymer = 2.49 * (inputs['ac|weights|W_fuel_max']*1.0/Kfsp)**0.726*(0.5)**0.363 - outputs['W_fuelsystem'] = W_fs_Raymer + W_fs_Raymer = 2.49 * (inputs["ac|weights|W_fuel_max"] * 1.0 / Kfsp) ** 0.726 * (0.5) ** 0.363 + outputs["W_fuelsystem"] = W_fs_Raymer def compute_partials(self, inputs, J): - Kfsp = self.options['Kfsp'] - J['W_fuelsystem','ac|weights|W_fuel_max'] = 2.49 * 0.726 *(inputs['ac|weights|W_fuel_max']*1.0/Kfsp)**(0.726-1) * (1.0/Kfsp) * (0.5)**0.363 + Kfsp = self.options["Kfsp"] + J["W_fuelsystem", "ac|weights|W_fuel_max"] = ( + 2.49 * 0.726 * (inputs["ac|weights|W_fuel_max"] * 1.0 / Kfsp) ** (0.726 - 1) * (1.0 / Kfsp) * (0.5) ** 0.363 + ) + class EquipmentWeight_SmallTurboprop(ExplicitComponent): def setup(self): - self.add_input('ac|weights|MTOW', units='lb',desc='Max takeoff weight') - self.add_input('ac|num_passengers_max',desc='Number of passengers') - self.add_input('ac|geom|fuselage|length', units='ft', desc='fuselage width') - self.add_input('ac|geom|wing|AR', desc='Wing aspect ratio') - self.add_input('ac|geom|wing|S_ref', units='ft**2', desc='Wing reference area') - self.add_input('W_fuelsystem', units='lb', desc='Fuel system weight') - self.add_output('W_equipment', units='lb',desc='Equipment weight') - self.declare_partials(['W_equipment'], ['*']) + self.add_input("ac|weights|MTOW", units="lb", desc="Max takeoff weight") + self.add_input("ac|num_passengers_max", desc="Number of passengers") + self.add_input("ac|geom|fuselage|length", units="ft", desc="fuselage width") + self.add_input("ac|geom|wing|AR", desc="Wing aspect ratio") + self.add_input("ac|geom|wing|S_ref", units="ft**2", desc="Wing reference area") + self.add_input("W_fuelsystem", units="lb", desc="Fuel system weight") + self.add_output("W_equipment", units="lb", desc="Equipment weight") + self.declare_partials(["W_equipment"], ["*"]) + def compute(self, inputs, outputs): - b = math.sqrt(inputs['ac|geom|wing|S_ref']*inputs['ac|geom|wing|AR']) - - #Flight control system (unpowered) - #Roskam PVC7p98eq7.2 - #Wfc_USAF = 1.066*inputs['ac|weights|MTOW']**0.626 - Wfc_Torenbeek = 0.23*inputs['ac|weights|MTOW']**0.666 - #Hydraulic system weight included in flight controls and LG weight - Whydraulics = 0.2673*1*(inputs['ac|geom|fuselage|length']*b)**0.937 - - #Guesstimate of avionics weight - #This is a guess for a single turboprop class airplane (such as TBM, Pilatus, etc) - Wavionics = 2.117*(np.array([110]))**0.933 - #Electrical system weight (NOT including elec propulsion) - Welec = 12.57*(inputs['W_fuelsystem']+Wavionics)**0.51 - - #pressurization and air conditioning from Roskam - Wapi = 0.265*inputs['ac|weights|MTOW']**0.52 * inputs['ac|num_passengers_max']**0.68 * Wavionics**0.17 * 0.95 - Woxygen = 30 + 1.2*inputs['ac|num_passengers_max'] - #furnishings (Cessna method) - Wfur = 0.412*inputs['ac|num_passengers_max']**1.145 * inputs['ac|weights|MTOW'] ** 0.489 - Wpaint = 0.003 * inputs['ac|weights|MTOW'] - - outputs['W_equipment'] = Wfc_Torenbeek + Welec + Wavionics + Wapi + Woxygen + Wfur + Wpaint + Whydraulics + b = math.sqrt(inputs["ac|geom|wing|S_ref"] * inputs["ac|geom|wing|AR"]) + + # Flight control system (unpowered) + # Roskam PVC7p98eq7.2 + # Wfc_USAF = 1.066*inputs['ac|weights|MTOW']**0.626 + Wfc_Torenbeek = 0.23 * inputs["ac|weights|MTOW"] ** 0.666 + # Hydraulic system weight included in flight controls and LG weight + Whydraulics = 0.2673 * 1 * (inputs["ac|geom|fuselage|length"] * b) ** 0.937 + + # Guesstimate of avionics weight + # This is a guess for a single turboprop class airplane (such as TBM, Pilatus, etc) + Wavionics = 2.117 * (np.array([110])) ** 0.933 + # Electrical system weight (NOT including elec propulsion) + Welec = 12.57 * (inputs["W_fuelsystem"] + Wavionics) ** 0.51 + + # pressurization and air conditioning from Roskam + Wapi = ( + 0.265 + * inputs["ac|weights|MTOW"] ** 0.52 + * inputs["ac|num_passengers_max"] ** 0.68 + * Wavionics**0.17 + * 0.95 + ) + Woxygen = 30 + 1.2 * inputs["ac|num_passengers_max"] + # furnishings (Cessna method) + Wfur = 0.412 * inputs["ac|num_passengers_max"] ** 1.145 * inputs["ac|weights|MTOW"] ** 0.489 + Wpaint = 0.003 * inputs["ac|weights|MTOW"] + + outputs["W_equipment"] = Wfc_Torenbeek + Welec + Wavionics + Wapi + Woxygen + Wfur + Wpaint + Whydraulics def compute_partials(self, inputs, J): - b = math.sqrt(inputs['ac|geom|wing|S_ref']*inputs['ac|geom|wing|AR']) - Wavionics = 2.117*(np.array([110]))**0.933 - J['W_equipment','ac|weights|MTOW'] = 0.23*inputs['ac|weights|MTOW']**(0.666-1)*0.666 + 0.52 * 0.265*inputs['ac|weights|MTOW']**(0.52-1) * inputs['ac|num_passengers_max']**0.68 * Wavionics**0.17 * 0.95 + 0.412*inputs['ac|num_passengers_max']**1.145 * inputs['ac|weights|MTOW'] ** (0.489-1) * 0.489 + 0.003 - J['W_equipment','ac|num_passengers_max'] = 0.265*inputs['ac|weights|MTOW']**0.52 * 0.68 * inputs['ac|num_passengers_max']**(0.68-1) * Wavionics**0.17 * 0.95 + 1.2 + 0.412*1.145 * inputs['ac|num_passengers_max']**(1.145-1) * inputs['ac|weights|MTOW'] ** 0.489 - J['W_equipment','ac|geom|fuselage|length'] = 0.2673*1*0.937 * (inputs['ac|geom|fuselage|length']*b)**(0.937 - 1) * b - J['W_equipment','W_fuelsystem'] = 12.57*(inputs['W_fuelsystem']+Wavionics)**(0.51-1) * 0.51 - J['W_equipment','ac|geom|wing|S_ref'] = 0.2673*1*(inputs['ac|geom|fuselage|length']*b)**(0.937-1)*0.937*inputs['ac|geom|fuselage|length']*(1/2)*1/b*inputs['ac|geom|wing|AR'] - J['W_equipment','ac|geom|wing|AR'] = 0.2673*1*(inputs['ac|geom|fuselage|length']*b)**(0.937-1)*0.937*inputs['ac|geom|fuselage|length']*(1/2)/b*inputs['ac|geom|wing|S_ref'] + b = math.sqrt(inputs["ac|geom|wing|S_ref"] * inputs["ac|geom|wing|AR"]) + Wavionics = 2.117 * (np.array([110])) ** 0.933 + J["W_equipment", "ac|weights|MTOW"] = ( + 0.23 * inputs["ac|weights|MTOW"] ** (0.666 - 1) * 0.666 + + 0.52 + * 0.265 + * inputs["ac|weights|MTOW"] ** (0.52 - 1) + * inputs["ac|num_passengers_max"] ** 0.68 + * Wavionics**0.17 + * 0.95 + + 0.412 * inputs["ac|num_passengers_max"] ** 1.145 * inputs["ac|weights|MTOW"] ** (0.489 - 1) * 0.489 + + 0.003 + ) + J["W_equipment", "ac|num_passengers_max"] = ( + 0.265 + * inputs["ac|weights|MTOW"] ** 0.52 + * 0.68 + * inputs["ac|num_passengers_max"] ** (0.68 - 1) + * Wavionics**0.17 + * 0.95 + + 1.2 + + 0.412 * 1.145 * inputs["ac|num_passengers_max"] ** (1.145 - 1) * inputs["ac|weights|MTOW"] ** 0.489 + ) + J["W_equipment", "ac|geom|fuselage|length"] = ( + 0.2673 * 1 * 0.937 * (inputs["ac|geom|fuselage|length"] * b) ** (0.937 - 1) * b + ) + J["W_equipment", "W_fuelsystem"] = 12.57 * (inputs["W_fuelsystem"] + Wavionics) ** (0.51 - 1) * 0.51 + J["W_equipment", "ac|geom|wing|S_ref"] = ( + 0.2673 + * 1 + * (inputs["ac|geom|fuselage|length"] * b) ** (0.937 - 1) + * 0.937 + * inputs["ac|geom|fuselage|length"] + * (1 / 2) + * 1 + / b + * inputs["ac|geom|wing|AR"] + ) + J["W_equipment", "ac|geom|wing|AR"] = ( + 0.2673 + * 1 + * (inputs["ac|geom|fuselage|length"] * b) ** (0.937 - 1) + * 0.937 + * inputs["ac|geom|fuselage|length"] + * (1 / 2) + / b + * inputs["ac|geom|wing|S_ref"] + ) -class SingleTurboPropEmptyWeight(Group): +class SingleTurboPropEmptyWeight(Group): def setup(self): - const = self.add_subsystem('const',IndepVarComp(),promotes_outputs=["*"]) - const.add_output('W_fluids', val=20, units='kg') - const.add_output('structural_fudge', val=1.6, units='m/m') - self.add_subsystem('wing',WingWeight_SmallTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('empennage',EmpennageWeight_SmallTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('fuselage',FuselageWeight_SmallTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('nacelle',NacelleWeight_SmallSingleTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('gear',LandingGearWeight_SmallTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('fuelsystem', FuelSystemWeight_SmallTurboprop(), promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('equipment',EquipmentWeight_SmallTurboprop(), promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('structural',AddSubtractComp(output_name='W_structure',input_names=['W_wing','W_fuselage','W_nacelle','W_empennage','W_gear'], units='lb'),promotes_outputs=['*'],promotes_inputs=["*"]) - self.add_subsystem('structural_fudge',ElementMultiplyDivideComp(output_name='W_structure_adjusted',input_names=['W_structure','structural_fudge'],input_units=['lb','m/m']),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('totalempty',AddSubtractComp(output_name='OEW',input_names=['W_structure_adjusted','W_fuelsystem','W_equipment','W_engine','W_propeller','W_fluids'], units='lb'),promotes_outputs=['*'],promotes_inputs=["*"]) + const = self.add_subsystem("const", IndepVarComp(), promotes_outputs=["*"]) + const.add_output("W_fluids", val=20, units="kg") + const.add_output("structural_fudge", val=1.6, units="m/m") + self.add_subsystem("wing", WingWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("empennage", EmpennageWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("fuselage", FuselageWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "nacelle", NacelleWeight_SmallSingleTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("gear", LandingGearWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "fuelsystem", FuelSystemWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("equipment", EquipmentWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "structural", + AddSubtractComp( + output_name="W_structure", + input_names=["W_wing", "W_fuselage", "W_nacelle", "W_empennage", "W_gear"], + units="lb", + ), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + self.add_subsystem( + "structural_fudge", + ElementMultiplyDivideComp( + output_name="W_structure_adjusted", + input_names=["W_structure", "structural_fudge"], + input_units=["lb", "m/m"], + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "totalempty", + AddSubtractComp( + output_name="OEW", + input_names=[ + "W_structure_adjusted", + "W_fuelsystem", + "W_equipment", + "W_engine", + "W_propeller", + "W_fluids", + ], + units="lb", + ), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) if __name__ == "__main__": from openmdao.api import IndepVarComp, Problem + prob = Problem() prob.model = Group() - dvs = prob.model.add_subsystem('dvs',IndepVarComp(),promotes_outputs=["*"]) - AR = 41.5**2/193.75 - dvs.add_output('ac|weights|MTOW',7394.0, units='lb') - dvs.add_output('ac|geom|wing|S_ref',193.75, units='ft**2') - dvs.add_output('ac|geom|wing|AR',AR) - dvs.add_output('ac|geom|wing|c4sweep',1.0, units='deg') - dvs.add_output('ac|geom|wing|taper',0.622) - dvs.add_output('ac|geom|wing|toverc',0.16) - #dvs.add_output('V_H',255, units='kn') - - dvs.add_output('ac|geom|hstab|S_ref',47.5, units='ft**2') - #dvs.add_output('AR_h',4.13) - dvs.add_output('ac|geom|vstab|S_ref',31.36, units='ft**2') - #dvs.add_output('AR_v',1.2) - #dvs.add_output('troot_h',0.8, units='ft') - #dvs.add_output('troot_v',0.8, units='ft') - dvs.add_output('ac|geom|hstab|c4_to_wing_c4',17.9, units='ft') - - dvs.add_output('ac|geom|fuselage|length',27.39, units='ft') - dvs.add_output('ac|geom|fuselage|height',5.555, units='ft') - dvs.add_output('ac|geom|fuselage|width',4.58, units='ft') - dvs.add_output('ac|geom|fuselage|S_wet',392, units='ft**2') - #dvs.add_output('V_C',201, units='kn') #IAS (converted from 315kt true at 28,000 ) - #dvs.add_output('V_MO',266, units='kn') - dvs.add_output('P_TO',850, units='hp') - dvs.add_output('ac|weights|W_fuel_max',2000, units='lb') - dvs.add_output('ac|num_passengers_max', 6) - dvs.add_output('ac|q_cruise', 135.4, units='lb*ft**-2') - dvs.add_output('ac|weights|MLW', 7000, units='lb') - dvs.add_output('ac|geom|nosegear|length', 3, units='ft') - dvs.add_output('ac|geom|maingear|length', 4, units='ft') - dvs.add_output('W_engine',475, units='lb') - dvs.add_output('W_propeller',150, units='lb') - - prob.model.add_subsystem('OEW',SingleTurboPropEmptyWeight(),promotes_inputs=["*"]) - + dvs = prob.model.add_subsystem("dvs", IndepVarComp(), promotes_outputs=["*"]) + AR = 41.5**2 / 193.75 + dvs.add_output("ac|weights|MTOW", 7394.0, units="lb") + dvs.add_output("ac|geom|wing|S_ref", 193.75, units="ft**2") + dvs.add_output("ac|geom|wing|AR", AR) + dvs.add_output("ac|geom|wing|c4sweep", 1.0, units="deg") + dvs.add_output("ac|geom|wing|taper", 0.622) + dvs.add_output("ac|geom|wing|toverc", 0.16) + # dvs.add_output('V_H',255, units='kn') + + dvs.add_output("ac|geom|hstab|S_ref", 47.5, units="ft**2") + # dvs.add_output('AR_h',4.13) + dvs.add_output("ac|geom|vstab|S_ref", 31.36, units="ft**2") + # dvs.add_output('AR_v',1.2) + # dvs.add_output('troot_h',0.8, units='ft') + # dvs.add_output('troot_v',0.8, units='ft') + dvs.add_output("ac|geom|hstab|c4_to_wing_c4", 17.9, units="ft") + + dvs.add_output("ac|geom|fuselage|length", 27.39, units="ft") + dvs.add_output("ac|geom|fuselage|height", 5.555, units="ft") + dvs.add_output("ac|geom|fuselage|width", 4.58, units="ft") + dvs.add_output("ac|geom|fuselage|S_wet", 392, units="ft**2") + # dvs.add_output('V_C',201, units='kn') #IAS (converted from 315kt true at 28,000 ) + # dvs.add_output('V_MO',266, units='kn') + dvs.add_output("P_TO", 850, units="hp") + dvs.add_output("ac|weights|W_fuel_max", 2000, units="lb") + dvs.add_output("ac|num_passengers_max", 6) + dvs.add_output("ac|q_cruise", 135.4, units="lb*ft**-2") + dvs.add_output("ac|weights|MLW", 7000, units="lb") + dvs.add_output("ac|geom|nosegear|length", 3, units="ft") + dvs.add_output("ac|geom|maingear|length", 4, units="ft") + dvs.add_output("W_engine", 475, units="lb") + dvs.add_output("W_propeller", 150, units="lb") + + prob.model.add_subsystem("OEW", SingleTurboPropEmptyWeight(), promotes_inputs=["*"]) # prob.model.add_subsystem('wing',WingWeight_SmallTurboprop(),promotes_inputs=["*"]) # prob.model.add_subsystem('empennage',EmpennageWeight_SmallTurboprop(),promotes_inputs=["*"]) @@ -406,24 +741,22 @@ def setup(self): # prob.model.add_subsystem('fuelsystem', FuelSystemWeight_SmallTurboprop(), promotes_inputs=["*"]) # prob.model.add_subsystem('equipment',EquipmentWeight_SmallTurboprop(), promotes_inputs=["*"]) - prob.setup() prob.run_model() - print('Wing weight:') - print(prob['OEW.W_wing']) - print('Fuselage weight:') - print(prob['OEW.W_fuselage']) - print('Empennage weight:') - print(prob['OEW.W_empennage']) - print('Nacelle weight:') - print(prob['OEW.W_nacelle']) - print('Fuel system weight') - print(prob['OEW.W_fuelsystem']) - print('Gear weight') - print(prob['OEW.W_gear']) - print('Equipment weight') - print(prob['OEW.W_equipment']) - print('Operating empty weight:') - print(prob['OEW.OEW']) + print("Wing weight:") + print(prob["OEW.W_wing"]) + print("Fuselage weight:") + print(prob["OEW.W_fuselage"]) + print("Empennage weight:") + print(prob["OEW.W_empennage"]) + print("Nacelle weight:") + print(prob["OEW.W_nacelle"]) + print("Fuel system weight") + print(prob["OEW.W_fuelsystem"]) + print("Gear weight") + print(prob["OEW.W_gear"]) + print("Equipment weight") + print(prob["OEW.W_equipment"]) + print("Operating empty weight:") + print(prob["OEW.OEW"]) data = prob.check_partials(compact_print=True) - diff --git a/openconcept/weights/weights_twin_hybrid.py b/openconcept/weights/weights_twin_hybrid.py index 54ef288c..7aaf27dc 100644 --- a/openconcept/weights/weights_twin_hybrid.py +++ b/openconcept/weights/weights_twin_hybrid.py @@ -10,18 +10,59 @@ EquipmentWeight_SmallTurboprop, ) + class TwinSeriesHybridEmptyWeight(Group): def setup(self): - const = self.add_subsystem('const',IndepVarComp(),promotes_outputs=["*"]) - const.add_output('W_fluids', val=20, units='kg') - const.add_output('structural_fudge', val=1.6, units='m/m') - self.add_subsystem('wing',WingWeight_SmallTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('empennage',EmpennageWeight_SmallTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('fuselage',FuselageWeight_SmallTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('nacelle',NacelleWeight_SmallSingleTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('gear',LandingGearWeight_SmallTurboprop(),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('fuelsystem', FuelSystemWeight_SmallTurboprop(), promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('equipment',EquipmentWeight_SmallTurboprop(), promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('structural',AddSubtractComp(output_name='W_structure',input_names=['W_wing','W_fuselage','W_nacelle','W_empennage','W_gear'], units='lb'),promotes_outputs=['*'],promotes_inputs=["*"]) - self.add_subsystem('structural_fudge',ElementMultiplyDivideComp(output_name='W_structure_adjusted',input_names=['W_structure','structural_fudge'],input_units=['lb','m/m']),promotes_inputs=["*"],promotes_outputs=["*"]) - self.add_subsystem('totalempty',AddSubtractComp(output_name='OEW',input_names=['W_structure_adjusted','W_fuelsystem','W_equipment','W_engine','W_motors','W_generator','W_propeller','W_fluids'], units='lb'),promotes_outputs=['*'],promotes_inputs=["*"]) + const = self.add_subsystem("const", IndepVarComp(), promotes_outputs=["*"]) + const.add_output("W_fluids", val=20, units="kg") + const.add_output("structural_fudge", val=1.6, units="m/m") + self.add_subsystem("wing", WingWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("empennage", EmpennageWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem("fuselage", FuselageWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "nacelle", NacelleWeight_SmallSingleTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("gear", LandingGearWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "fuelsystem", FuelSystemWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"] + ) + self.add_subsystem("equipment", EquipmentWeight_SmallTurboprop(), promotes_inputs=["*"], promotes_outputs=["*"]) + self.add_subsystem( + "structural", + AddSubtractComp( + output_name="W_structure", + input_names=["W_wing", "W_fuselage", "W_nacelle", "W_empennage", "W_gear"], + units="lb", + ), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) + self.add_subsystem( + "structural_fudge", + ElementMultiplyDivideComp( + output_name="W_structure_adjusted", + input_names=["W_structure", "structural_fudge"], + input_units=["lb", "m/m"], + ), + promotes_inputs=["*"], + promotes_outputs=["*"], + ) + self.add_subsystem( + "totalempty", + AddSubtractComp( + output_name="OEW", + input_names=[ + "W_structure_adjusted", + "W_fuelsystem", + "W_equipment", + "W_engine", + "W_motors", + "W_generator", + "W_propeller", + "W_fluids", + ], + units="lb", + ), + promotes_outputs=["*"], + promotes_inputs=["*"], + ) diff --git a/setup.py b/setup.py index b78a82da..3df39866 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ __version__ = re.findall( r"""__version__ = ["']+([0-9\.]*)["']+""", - open('openconcept/__init__.py').read(), + open("openconcept/__init__.py").read(), )[0] this_directory = os.path.abspath(os.path.dirname(__file__)) @@ -12,39 +12,39 @@ long_description = f.read() setup( - name='openconcept', + name="openconcept", version=__version__, description="Open aircraft conceptual design tools", long_description=long_description, long_description_content_type="text/markdown", classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: POSIX :: Linux', - 'Operating System :: Microsoft :: Windows', - 'Topic :: Scientific/Engineering', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: Implementation :: CPython', + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Scientific/Engineering", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: Implementation :: CPython", ], - keywords='aircraft design optimization multidisciplinary multi-disciplinary analysis', - author='Benjamin J. Brelje and Eytan J. Adler', - author_email='', - url='https://github.com/mdolab/openconcept', - download_url='https://github.com/mdolab/openconcept', - license='MIT License', + keywords="aircraft design optimization multidisciplinary multi-disciplinary analysis", + author="Benjamin J. Brelje and Eytan J. Adler", + author_email="", + url="https://github.com/mdolab/openconcept", + download_url="https://github.com/mdolab/openconcept", + license="MIT License", packages=find_packages(include=["openconcept*"]), install_requires=[ - 'scipy>=1.0.0', - 'numpy>=1.14.0', - 'openmdao>=3.10.0', + "scipy>=1.0.0", + "numpy>=1.14.0", + "openmdao>=3.10.0", ], - extras_require = { - 'testing': ["pytest", "pytest-cov", "coverage", "openaerostruct"], - 'docs': ["sphinx_mdolab_theme", "openaerostruct"], - 'plot': ["matplotlib"] - }, + extras_require={ + "testing": ["pytest", "pytest-cov", "coverage", "openaerostruct"], + "docs": ["sphinx_mdolab_theme", "openaerostruct"], + "plot": ["matplotlib"], + }, ) From 13b6b116ef85191d717263aa5eaec6a423cbc768 Mon Sep 17 00:00:00 2001 From: eytanadler Date: Thu, 11 Aug 2022 10:03:29 -0400 Subject: [PATCH 2/7] Fixed some flake8 stuff --- .gitignore | 1 + .../openaerostruct/aerostructural.py | 18 +++--- .../aerodynamics/openaerostruct/drag_polar.py | 12 ++-- .../tests/test_aerostructural.py | 6 +- .../openaerostruct/tests/test_drag_polar.py | 9 ++- openconcept/costs/costs_commuter.py | 1 - openconcept/energy_storage/battery.py | 3 - .../energy_storage/tests/test_battery.py | 2 +- openconcept/examples/B738_VLM_drag.py | 3 +- .../examples/ElectricSinglewithThermal.py | 18 +----- openconcept/examples/HybridTwin.py | 12 ++-- .../propulsion/empirical_data/prop_maps.py | 2 +- openconcept/propulsion/generator.py | 2 - openconcept/propulsion/motor.py | 3 - openconcept/propulsion/propeller.py | 4 -- openconcept/propulsion/splitter.py | 3 - .../systems/simple_series_hybrid.py | 57 +------------------ .../systems/thermal_series_hybrid.py | 57 +------------------ .../propulsion/tests/test_simple_comps.py | 2 +- .../propulsion/tests/test_splitter_comps.py | 2 +- openconcept/propulsion/turboshaft.py | 8 --- 21 files changed, 37 insertions(+), 188 deletions(-) diff --git a/.gitignore b/.gitignore index 992c73dc..cdc922b5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ scratch/ *.zip *.npy .DS_Store +.flake8 diff --git a/openconcept/aerodynamics/openaerostruct/aerostructural.py b/openconcept/aerodynamics/openaerostruct/aerostructural.py index 6a51862b..8a011396 100644 --- a/openconcept/aerodynamics/openaerostruct/aerostructural.py +++ b/openconcept/aerodynamics/openaerostruct/aerostructural.py @@ -5,6 +5,12 @@ import multiprocessing as mp import warnings +# Atmospheric calculations +from openconcept.atmospherics import TemperatureComp, PressureComp, DensityComp, SpeedOfSoundComp + +# Utitilty for vector manipulation +from openconcept.utilities import VectorConcatenateComp + # Progress bar progress_bar = True try: @@ -23,12 +29,6 @@ except ImportError: raise ImportError("OpenAeroStruct must be installed to use the AerostructDragPolar component") -# Atmospheric calculations -from openconcept.atmospherics import TemperatureComp, PressureComp, DensityComp, SpeedOfSoundComp - -# Utitilty for vector manipulation -from openconcept.utilities import VectorConcatenateComp - CITATION = """ @InProceedings{Adler2022a, author = {Eytan J. Adler and Joaquim R. R. A. Martins}, @@ -588,7 +588,7 @@ def compute_partials(self, inputs, partials): def compute_training_data(inputs, surf_dict=None): t_start = time() - print(f"Generating OpenAeroStruct aerostructural training data...") + print("Generating OpenAeroStruct aerostructural training data...") # Set up test points for use in parallelized map function ([Mach, alpha, altitude, inputs] for each point) test_points = np.array( @@ -1560,7 +1560,7 @@ def example_usage(): p.run_model() - print(f"================== SURROGATE ==================") + print("================== SURROGATE ==================") print(f"CL: {p.get_val('aero_surrogate.CL')}") print(f"CD: {p.get_val('aero_surrogate.CD')}") print(f"Alpha: {p.get_val('aero_surrogate.alpha', units='deg')} deg") @@ -1597,7 +1597,7 @@ def example_usage(): prob.run_model() - print(f"================== OpenAeroStruct ==================") + print("================== OpenAeroStruct ==================") print(f"CL: {prob.get_val('fltcond|CL')}") print(f"CD: {prob.get_val('fltcond|CD')}") print(f"Alpha: {prob.get_val('fltcond|alpha', units='deg')} deg") diff --git a/openconcept/aerodynamics/openaerostruct/drag_polar.py b/openconcept/aerodynamics/openaerostruct/drag_polar.py index c9b3b91d..20157938 100644 --- a/openconcept/aerodynamics/openaerostruct/drag_polar.py +++ b/openconcept/aerodynamics/openaerostruct/drag_polar.py @@ -4,6 +4,9 @@ from copy import copy, deepcopy import multiprocessing as mp +# Atmospheric calculations +from openconcept.atmospherics import TemperatureComp, PressureComp, DensityComp, SpeedOfSoundComp + # Progress bar progress_bar = True try: @@ -19,9 +22,6 @@ except ImportError: raise ImportError("OpenAeroStruct must be installed to use the VLMDragPolar component") -# Atmospheric calculations -from openconcept.atmospherics import TemperatureComp, PressureComp, DensityComp, SpeedOfSoundComp - CITATION = """ @InProceedings{Adler2022a, author = {Eytan J. Adler and Joaquim R. R. A. Martins}, @@ -455,7 +455,7 @@ def compute_partials(self, inputs, partials): def compute_training_data(inputs, surf_dict=None): t_start = time() - print(f"Generating OpenAeroStruct aerodynamic training data...") + print("Generating OpenAeroStruct aerodynamic training data...") # Set up test points for use in parallelized map function ([Mach, alpha, altitude, inputs] for each point) test_points = np.array( @@ -979,7 +979,7 @@ def example_usage(): p.run_model() - print(f"================== SURROGATE ==================") + print("================== SURROGATE ==================") print(f"CL: {p.get_val('aero_surrogate.CL')}") print(f"CD: {p.get_val('aero_surrogate.CD')}") print(f"Alpha: {p.get_val('aero_surrogate.alpha', units='deg')} deg") @@ -1005,7 +1005,7 @@ def example_usage(): prob.run_model() - print(f"\n================== OpenAeroStruct ==================") + print("\n================== OpenAeroStruct ==================") print(f"CL: {prob.get_val('fltcond|CL')}") print(f"CD: {prob.get_val('fltcond|CD')}") print(f"Alpha: {prob.get_val('fltcond|alpha', units='deg')} deg") diff --git a/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py b/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py index e91aeb48..c32eb0f6 100644 --- a/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py +++ b/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py @@ -5,12 +5,10 @@ # Only run if OpenAeroStruct is installed try: - from openaerostruct.geometry.geometry_group import Geometry - from openaerostruct.aerodynamics.aero_groups import AeroPoint - from openconcept.aerodynamics.openaerostruct.aerostructural import * + from openconcept.aerodynamics.openaerostruct.aerostructural import OASDataGen, Aerostruct, AerostructDragPolar, AerostructDragPolarExact, example_usage OAS_installed = True -except: +except ImportError: OAS_installed = False diff --git a/openconcept/aerodynamics/openaerostruct/tests/test_drag_polar.py b/openconcept/aerodynamics/openaerostruct/tests/test_drag_polar.py index 14c2555c..38237a49 100644 --- a/openconcept/aerodynamics/openaerostruct/tests/test_drag_polar.py +++ b/openconcept/aerodynamics/openaerostruct/tests/test_drag_polar.py @@ -7,10 +7,10 @@ try: from openaerostruct.geometry.geometry_group import Geometry from openaerostruct.aerodynamics.aero_groups import AeroPoint - from openconcept.aerodynamics.openaerostruct.drag_polar import * + from openconcept.aerodynamics.openaerostruct.drag_polar import VLMDataGen, VLM, VLMDragPolar, PlanformMesh, example_usage OAS_installed = True -except: +except ImportError: OAS_installed = False @@ -508,7 +508,7 @@ def test_777ish_regression(self): assert_check_partials(partials, atol=2e-5) -def run_OAS(inputs, with_viscous=True, with_wave=True, t_over_c=np.array([0.12])): +def run_OAS(inputs, with_viscous=True, with_wave=True, t_over_c=None): """ Runs OpenAeroStruct with flight condition and mesh inputs. @@ -546,6 +546,9 @@ def run_OAS(inputs, with_viscous=True, with_wave=True, t_over_c=np.array([0.12]) CD : float Drag coefficient """ + if t_over_c is None: + t_over_c = np.array([0.12]) + # Create a dictionary with info and options about the aerodynamic # lifting surface surface = { diff --git a/openconcept/costs/costs_commuter.py b/openconcept/costs/costs_commuter.py index 0140d38a..da3b5276 100644 --- a/openconcept/costs/costs_commuter.py +++ b/openconcept/costs/costs_commuter.py @@ -4,7 +4,6 @@ """ from openmdao.api import ExplicitComponent -import numpy as np class TurbopropOperatingCost(ExplicitComponent): diff --git a/openconcept/energy_storage/battery.py b/openconcept/energy_storage/battery.py index ef60b254..06049f57 100644 --- a/openconcept/energy_storage/battery.py +++ b/openconcept/energy_storage/battery.py @@ -169,9 +169,7 @@ def setup(self): e_b = self.options["specific_energy"] self.add_input("specific_energy", units="W * h / kg", val=e_b) eta_b = self.options["efficiency"] - p_b = self.options["specific_power"] cost_inc = self.options["cost_inc"] - cost_base = self.options["cost_base"] self.add_output("heat_out", units="W", desc="Waste heat out", shape=(nn,)) self.add_output("component_cost", units="USD", desc="Battery cost") @@ -197,7 +195,6 @@ def compute(self, inputs, outputs): outputs["max_energy"] = inputs["battery_weight"] * e_b def compute_partials(self, inputs, J): - eta_b = self.options["efficiency"] p_b = self.options["specific_power"] e_b = inputs["specific_energy"] J["component_sizing_margin", "elec_load"] = 1 / (p_b * inputs["battery_weight"]) diff --git a/openconcept/energy_storage/tests/test_battery.py b/openconcept/energy_storage/tests/test_battery.py index 7cea6310..68144662 100644 --- a/openconcept/energy_storage/tests/test_battery.py +++ b/openconcept/energy_storage/tests/test_battery.py @@ -1,7 +1,7 @@ import unittest import numpy as np from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -from openmdao.api import IndepVarComp, Group, Problem, IndepVarComp +from openmdao.api import IndepVarComp, Group, Problem from openconcept.energy_storage import SimpleBattery diff --git a/openconcept/examples/B738_VLM_drag.py b/openconcept/examples/B738_VLM_drag.py index 73464bcd..d38f3632 100644 --- a/openconcept/examples/B738_VLM_drag.py +++ b/openconcept/examples/B738_VLM_drag.py @@ -39,7 +39,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - flight_phase = self.options["flight_phase"] # a propulsion system needs to be defined in order to provide thrust # information for the mission analysis code @@ -138,7 +137,7 @@ def setup(self): dv_comp.add_output_from_dict("ac|q_cruise") # Run a full mission analysis including takeoff, reserve_, cruise,reserve_ and descereserve_nt - analysis = self.add_subsystem( + self.add_subsystem( "analysis", MissionWithReserve(num_nodes=nn, aircraft_model=B738AirplaneModel), promotes_inputs=["*"], diff --git a/openconcept/examples/ElectricSinglewithThermal.py b/openconcept/examples/ElectricSinglewithThermal.py index e9358051..61184091 100644 --- a/openconcept/examples/ElectricSinglewithThermal.py +++ b/openconcept/examples/ElectricSinglewithThermal.py @@ -105,11 +105,7 @@ def setup(self): mission_data_comp.add_output("T_motor_initial", val=15, units="degC") mission_data_comp.add_output("T_res_initial", val=15.1, units="degC") - connect_phases_1 = ["v0v1", "v1vr", "rotate", "climb", "cruise", "descent"] - connect_states_1 = ["propmodel.batt1.SOC", "propmodel.motorheatsink.T", "propmodel.reservoir.T"] - extra_states_tuple_1 = [(connect_state, connect_phases_1) for connect_state in connect_states_1] - - analysis = self.add_subsystem( + self.add_subsystem( "analysis", FullMissionAnalysis(num_nodes=nn, aircraft_model=ElectricTBM850Model, transition_method="ode"), promotes_inputs=["*"], @@ -170,18 +166,6 @@ def show_outputs(prob): for i, thing in enumerate(vars_list): print(nice_print_names[i] + ": " + str(prob.get_val(thing, units=units[i])[0]) + " " + str(units[i])) - y_variables = [ - "fltcond|h", - "fltcond|Ueas", - "throttle", - "fltcond|vs", - "propmodel.batt1.SOC", - "propmodel.motorheatsink.T", - "propmodel.reservoir.T_out", - "propmodel.duct.mdot", - ] - y_units = ["ft", "kn", None, "ft/min", None, "degC", "degC", "kg/s"] - # plot some stuff plots = True if plots: diff --git a/openconcept/examples/HybridTwin.py b/openconcept/examples/HybridTwin.py index 706f0cc3..f4f0a7ff 100644 --- a/openconcept/examples/HybridTwin.py +++ b/openconcept/examples/HybridTwin.py @@ -2,9 +2,7 @@ import logging import numpy as np -from openmdao.api import Problem, Group, ScipyOptimizeDriver -from openmdao.api import BalanceComp, ExplicitComponent, ExecComp, SqliteRecorder -from openmdao.api import DirectSolver, IndepVarComp, NewtonSolver, BoundsEnforceLS +from openmdao.api import Problem, Group, ScipyOptimizeDriver, ExplicitComponent, ExecComp, SqliteRecorder, DirectSolver, IndepVarComp, NewtonSolver # imports for the airplane model itself from openconcept.aerodynamics import PolarDrag @@ -52,7 +50,7 @@ def setup(self): else: controls.add_output("hybridization", val=1.0) - hybrid_factor = self.add_subsystem( + self.add_subsystem( "hybrid_factor", LinearInterpolator(num_nodes=nn), promotes_inputs=[("start_val", "hybridization"), ("end_val", "hybridization")], @@ -167,14 +165,14 @@ def setup(self): mission_data_comp = self.add_subsystem("mission_data_comp", IndepVarComp(), promotes_outputs=["*"]) mission_data_comp.add_output("batt_soc_target", val=0.1, units=None) - analysis = self.add_subsystem( + self.add_subsystem( "analysis", FullMissionAnalysis(num_nodes=nn, aircraft_model=SeriesHybridTwinModel), promotes_inputs=["*"], promotes_outputs=["*"], ) - margins = self.add_subsystem( + self.add_subsystem( "margins", ExecComp( "MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload", @@ -192,7 +190,7 @@ def setup(self): self.connect("ac|weights|MTOW", "margins.MTOW") self.connect("ac|weights|W_battery", "margins.W_battery") - augobj = self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) + self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) self.connect("ac|weights|MTOW", "aug_obj.ac|weights|MTOW") self.connect("descent.fuel_used_final", "aug_obj.fuel_burn") diff --git a/openconcept/propulsion/empirical_data/prop_maps.py b/openconcept/propulsion/empirical_data/prop_maps.py index e3b45c48..89b21b39 100644 --- a/openconcept/propulsion/empirical_data/prop_maps.py +++ b/openconcept/propulsion/empirical_data/prop_maps.py @@ -1,5 +1,5 @@ import numpy as np -from openmdao.api import Group, Problem, IndepVarComp, ExplicitComponent +from openmdao.api import ExplicitComponent from openmdao.components.meta_model_structured_comp import MetaModelStructuredComp diff --git a/openconcept/propulsion/generator.py b/openconcept/propulsion/generator.py index 49b4e5f7..93e85b10 100644 --- a/openconcept/propulsion/generator.py +++ b/openconcept/propulsion/generator.py @@ -60,9 +60,7 @@ def setup(self): # outputs and partials eta_g = self.options["efficiency"] weight_inc = self.options["weight_inc"] - weight_base = self.options["weight_base"] cost_inc = self.options["cost_inc"] - cost_base = self.options["cost_base"] self.add_output("elec_power_out", units="W", desc="Output electric power", shape=(nn,)) self.add_output("heat_out", units="W", desc="Waste heat out", shape=(nn,)) diff --git a/openconcept/propulsion/motor.py b/openconcept/propulsion/motor.py index 2ffd02ac..45cea75d 100644 --- a/openconcept/propulsion/motor.py +++ b/openconcept/propulsion/motor.py @@ -60,11 +60,8 @@ def setup(self): self.add_input("elec_power_rating", units="W", desc="Rated electrical power (load)") # outputs and partials - eta_m = self.options["efficiency"] weight_inc = self.options["weight_inc"] - weight_base = self.options["weight_base"] cost_inc = self.options["cost_inc"] - cost_base = self.options["cost_base"] self.add_output("shaft_power_out", units="W", desc="Output shaft power", shape=(nn,)) self.add_output("heat_out", units="W", desc="Waste heat out", shape=(nn,)) diff --git a/openconcept/propulsion/propeller.py b/openconcept/propulsion/propeller.py index 6c098395..055f2b98 100644 --- a/openconcept/propulsion/propeller.py +++ b/openconcept/propulsion/propeller.py @@ -6,8 +6,6 @@ propeller_map_highpower, static_propeller_map_Raymer, static_propeller_map_highpower, - propeller_map_scaled, - propeller_map_constant_prop_efficiency, ) @@ -60,8 +58,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] n_blades = self.options["num_blades"] - design_J = self.options["design_J"] - design_cp = self.options["design_cp"] if n_blades == 3: propmap = propeller_map_Raymer(nn) staticpropmap = static_propeller_map_Raymer(nn) diff --git a/openconcept/propulsion/splitter.py b/openconcept/propulsion/splitter.py index 4123e0f5..65656031 100644 --- a/openconcept/propulsion/splitter.py +++ b/openconcept/propulsion/splitter.py @@ -1,6 +1,5 @@ import numpy as np from openmdao.api import ExplicitComponent -from openmdao.api import Group class PowerSplit(ExplicitComponent): @@ -86,9 +85,7 @@ def setup(self): eta = self.options["efficiency"] weight_inc = self.options["weight_inc"] - weight_base = self.options["weight_base"] cost_inc = self.options["cost_inc"] - cost_base = self.options["cost_base"] self.add_output("power_out_A", units="W", desc="Output power or load to A", shape=(nn,)) self.add_output("power_out_B", units="W", desc="Output power or load to B", shape=(nn,)) diff --git a/openconcept/propulsion/systems/simple_series_hybrid.py b/openconcept/propulsion/systems/simple_series_hybrid.py index 26cdd0d6..3700417d 100644 --- a/openconcept/propulsion/systems/simple_series_hybrid.py +++ b/openconcept/propulsion/systems/simple_series_hybrid.py @@ -2,7 +2,7 @@ from openconcept.energy_storage import SimpleBattery, SOCBattery from openconcept.utilities import DVLabel, AddSubtractComp, ElementMultiplyDivideComp -from openmdao.api import Problem, Group, IndepVarComp, BalanceComp +from openmdao.api import Group, BalanceComp import numpy as np @@ -336,58 +336,3 @@ def setup(self): self.connect("motor_rating", ["prop1.power_rating"]) self.connect("gen_rating", "gen1.elec_power_rating") self.connect("batt_weight", "batt1.battery_weight") - - -class VehicleSizingModel(Group): - def setup(self): - dvs = self.add_subsystem("dvs", IndepVarComp(), promotes_outputs=["*"]) - climb = self.add_subsystem("missionanalysis", MissionAnalysis(), promotes_inputs=["dv_*"]) - dvs.add_output("dv_prop1_diameter", 3.0, units="m") - dvs.add_output("dv_motor1_rating", 1.5, units="MW") - dvs.add_output("dv_gen1_rating", 1.55, units="MW") - dvs.add_output("ac|propulsion|engine|rating", 1.6, units="MW") - dvs.add_output("dv_batt1_weight", 2000, units="kg") - - -if __name__ == "__main__": - import matplotlib.pyplot as plt - - prob = Problem() - - prob.model = VehicleSizingModel() - - prob.setup() - prob.run_model() - - # print "------Prop 1-------" - print("Thrust: " + str(prob["missionanalysis.propmodel.prop1.thrust"])) - plt.plot(prob["missionanalysis.propmodel.prop1.thrust"]) - plt.show() - - print("Weight: " + str(prob["missionanalysis.propmodel.prop1.component_weight"])) - - # print'Prop eff: ' + str(prob['prop1.eta_prop']) - - # print "------Motor 1-------" - # print 'Shaft power: ' + str(prob['motor1.shaft_power_out']) - # print 'Elec load: ' + str(prob['motor1.elec_load']) - # print 'Heat: ' + str(prob['motor1.heat_out']) - - # print "------Battery-------" - # print 'Elec load: ' + str(prob['batt1.elec_load']) - # print 'Heat: ' + str(prob['batt1.heat_out']) - - # print "------Generator-------" - # print 'Shaft power: ' + str(prob['gen1.shaft_power_in']) - # print 'Elec load: ' + str(prob['gen1.elec_power_out']) - # print 'Heat: ' + str(prob['gen1.heat_out']) - - # print "------Turboshaft-------" - # print 'Throttle: ' + str(prob['eng1.throttle']) - # print 'Shaft power: ' + str(prob['eng1.shaft_power_out']) - # print 'Fuel flow:' + str(prob['eng1.fuel_flow']*60*60) - - # prob.model.list_inputs() - # prob.model.list_outputs() - # prob.check_partials(compact_print=True) - # prob.check_totals(compact_print=True) diff --git a/openconcept/propulsion/systems/thermal_series_hybrid.py b/openconcept/propulsion/systems/thermal_series_hybrid.py index 97111196..ff76f7e0 100644 --- a/openconcept/propulsion/systems/thermal_series_hybrid.py +++ b/openconcept/propulsion/systems/thermal_series_hybrid.py @@ -11,7 +11,7 @@ HXGroup, ) -from openmdao.api import Problem, Group, IndepVarComp, BalanceComp +from openmdao.api import Group, IndepVarComp, BalanceComp import numpy as np @@ -409,58 +409,3 @@ def setup(self): self.connect("hx.T_out_hot", "refrig.T_in_hot") self.connect("mdot_coolant", "hx.mdot_hot") - - -class VehicleSizingModel(Group): - def setup(self): - dvs = self.add_subsystem("dvs", IndepVarComp(), promotes_outputs=["*"]) - climb = self.add_subsystem("missionanalysis", MissionAnalysis(), promotes_inputs=["dv_*"]) - dvs.add_output("dv_prop1_diameter", 3.0, units="m") - dvs.add_output("dv_motor1_rating", 1.5, units="MW") - dvs.add_output("dv_gen1_rating", 1.55, units="MW") - dvs.add_output("ac|propulsion|engine|rating", 1.6, units="MW") - dvs.add_output("dv_batt1_weight", 2000, units="kg") - - -if __name__ == "__main__": - import matplotlib.pyplot as plt - - prob = Problem() - - prob.model = VehicleSizingModel() - - prob.setup() - prob.run_model() - - # print "------Prop 1-------" - print("Thrust: " + str(prob["missionanalysis.propmodel.prop1.thrust"])) - plt.plot(prob["missionanalysis.propmodel.prop1.thrust"]) - plt.show() - - print("Weight: " + str(prob["missionanalysis.propmodel.prop1.component_weight"])) - - # print'Prop eff: ' + str(prob['prop1.eta_prop']) - - # print "------Motor 1-------" - # print 'Shaft power: ' + str(prob['motor1.shaft_power_out']) - # print 'Elec load: ' + str(prob['motor1.elec_load']) - # print 'Heat: ' + str(prob['motor1.heat_out']) - - # print "------Battery-------" - # print 'Elec load: ' + str(prob['batt1.elec_load']) - # print 'Heat: ' + str(prob['batt1.heat_out']) - - # print "------Generator-------" - # print 'Shaft power: ' + str(prob['gen1.shaft_power_in']) - # print 'Elec load: ' + str(prob['gen1.elec_power_out']) - # print 'Heat: ' + str(prob['gen1.heat_out']) - - # print "------Turboshaft-------" - # print 'Throttle: ' + str(prob['eng1.throttle']) - # print 'Shaft power: ' + str(prob['eng1.shaft_power_out']) - # print 'Fuel flow:' + str(prob['eng1.fuel_flow']*60*60) - - # prob.model.list_inputs() - # prob.model.list_outputs() - # prob.check_partials(compact_print=True) - # prob.check_totals(compact_print=True) diff --git a/openconcept/propulsion/tests/test_simple_comps.py b/openconcept/propulsion/tests/test_simple_comps.py index dba00572..1df7832f 100644 --- a/openconcept/propulsion/tests/test_simple_comps.py +++ b/openconcept/propulsion/tests/test_simple_comps.py @@ -2,7 +2,7 @@ import numpy as np from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials from openmdao.api import IndepVarComp, Group, Problem -from openconcept.propulsion import SimpleGenerator, SimpleMotor, SimplePropeller, SimpleTurboshaft, PowerSplit +from openconcept.propulsion import SimpleGenerator, SimpleMotor, SimpleTurboshaft class MotorTestGroup(Group): diff --git a/openconcept/propulsion/tests/test_splitter_comps.py b/openconcept/propulsion/tests/test_splitter_comps.py index d3cfd980..142aaae6 100644 --- a/openconcept/propulsion/tests/test_splitter_comps.py +++ b/openconcept/propulsion/tests/test_splitter_comps.py @@ -1,7 +1,7 @@ import unittest import numpy as np from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -from openmdao.api import IndepVarComp, Group, Problem +from openmdao.api import Problem from openconcept.propulsion import PowerSplit diff --git a/openconcept/propulsion/turboshaft.py b/openconcept/propulsion/turboshaft.py index b0bd0f27..5e60a925 100644 --- a/openconcept/propulsion/turboshaft.py +++ b/openconcept/propulsion/turboshaft.py @@ -1,6 +1,5 @@ import numpy as np from openmdao.api import ExplicitComponent -from openmdao.api import Group class SimpleTurboshaft(ExplicitComponent): @@ -71,11 +70,8 @@ def setup(self): self.add_input("throttle", desc="Throttle input (Fractional)", shape=(nn,)) self.add_input("shaft_power_rating", units="W", desc="Rated shaft power") - psfc = self.options["psfc"] weight_inc = self.options["weight_inc"] - weight_base = self.options["weight_base"] cost_inc = self.options["cost_inc"] - cost_base = self.options["cost_base"] self.add_output("shaft_power_out", units="W", desc="Output shaft power", shape=(nn,)) self.add_output("fuel_flow", units="kg/s", desc="Fuel flow in (kg fuel / s)", shape=(nn,)) @@ -96,16 +92,12 @@ def setup(self): ) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] psfc = self.options["psfc"] weight_inc = self.options["weight_inc"] weight_base = self.options["weight_base"] cost_inc = self.options["cost_inc"] cost_base = self.options["cost_base"] - a = inputs["throttle"] - b = inputs["shaft_power_rating"] - c = a * b outputs["shaft_power_out"] = inputs["throttle"] * inputs["shaft_power_rating"] outputs["fuel_flow"] = inputs["throttle"] * inputs["shaft_power_rating"] * psfc outputs["component_cost"] = inputs["shaft_power_rating"] * cost_inc + cost_base From e88507723aef1902bfc4bfe98c18d6ad0e151a32 Mon Sep 17 00:00:00 2001 From: eytanadler Date: Thu, 11 Aug 2022 10:45:52 -0400 Subject: [PATCH 3/7] Finished flake8 complaints (at least locally) --- .../atmospherics/dynamic_pressure_comp.py | 2 - openconcept/atmospherics/pressure_comp.py | 4 -- openconcept/atmospherics/temperature_comp.py | 4 -- openconcept/examples/B738.py | 2 +- openconcept/examples/B738_aerostructural.py | 3 +- openconcept/examples/Caravan.py | 3 +- .../examples/HybridTwin_active_thermal.py | 9 ++-- openconcept/examples/HybridTwin_thermal.py | 9 ++-- openconcept/examples/KingAirC90GT.py | 6 +-- .../examples/N3_HybridSingleAisle_Refrig.py | 5 +- .../aircraft_data/HybridSingleAisle.py | 1 - .../examples/tests/test_example_aircraft.py | 3 +- openconcept/mission/phases.py | 6 +-- .../tests/test_solver_phase_helpers.py | 17 ------- .../mission/tests/test_trajectories.py | 48 +++++++++---------- openconcept/thermal/battery_cooling.py | 5 +- openconcept/thermal/chiller.py | 4 +- openconcept/thermal/ducts.py | 29 +---------- openconcept/thermal/heat_exchanger.py | 6 --- openconcept/thermal/heat_pipe.py | 1 - openconcept/thermal/hose.py | 3 -- openconcept/thermal/pump.py | 3 -- .../thermal/tests/test_battery_cooling.py | 3 +- openconcept/thermal/tests/test_ducts.py | 8 ++-- .../thermal/tests/test_heat_exchanger.py | 4 +- openconcept/thermal/tests/test_heat_pipe.py | 1 - openconcept/thermal/tests/test_manifold.py | 2 +- openconcept/thermal/tests/test_pump.py | 2 +- openconcept/thermal/thermal.py | 2 - .../utilities/math/add_subtract_comp.py | 2 +- .../utilities/math/combine_split_comp.py | 11 ++--- openconcept/utilities/math/derivatives.py | 19 +------- openconcept/utilities/math/integrals.py | 20 ++------ .../utilities/math/multiply_divide_comp.py | 18 +++---- .../math/tests/test_combine_split.py | 4 +- .../utilities/math/tests/test_integrals.py | 11 ----- openconcept/utilities/math/tests/test_math.py | 2 - .../math/tests/test_multiply_divide_comp.py | 1 - openconcept/utilities/selector.py | 2 - .../utilities/tests/test_dict_indepvarcomp.py | 2 - openconcept/utilities/tests/test_dvlabel.py | 1 - openconcept/utilities/visualization.py | 3 +- openconcept/weights/weights_turboprop.py | 8 +--- 43 files changed, 78 insertions(+), 221 deletions(-) diff --git a/openconcept/atmospherics/dynamic_pressure_comp.py b/openconcept/atmospherics/dynamic_pressure_comp.py index 5279677e..5134d06f 100644 --- a/openconcept/atmospherics/dynamic_pressure_comp.py +++ b/openconcept/atmospherics/dynamic_pressure_comp.py @@ -40,10 +40,8 @@ def setup(self): self.declare_partials("fltcond|q", "fltcond|Utrue", rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] outputs["fltcond|q"] = 0.5 * inputs["fltcond|rho"] * inputs["fltcond|Utrue"] ** 2 def compute_partials(self, inputs, partials): - nn = self.options["num_nodes"] partials["fltcond|q", "fltcond|rho"] = 0.5 * inputs["fltcond|Utrue"] ** 2 partials["fltcond|q", "fltcond|Utrue"] = inputs["fltcond|rho"] * inputs["fltcond|Utrue"] diff --git a/openconcept/atmospherics/pressure_comp.py b/openconcept/atmospherics/pressure_comp.py index 669ec0cc..31be2ab9 100644 --- a/openconcept/atmospherics/pressure_comp.py +++ b/openconcept/atmospherics/pressure_comp.py @@ -42,8 +42,6 @@ def setup(self): self.declare_partials("fltcond|p", "fltcond|h", rows=arange, cols=arange) def compute(self, inputs, outputs): - num_points = self.options["num_nodes"] - h_m = inputs["fltcond|h"] self.tropos_mask, self.strato_mask, self.smooth_mask = get_mask_arrays(h_m) p_Pa = compute_pressures(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) @@ -51,8 +49,6 @@ def compute(self, inputs, outputs): outputs["fltcond|p"] = p_Pa def compute_partials(self, inputs, partials): - num_points = self.options["num_nodes"] - h_m = inputs["fltcond|h"] derivs = compute_pressure_derivs(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) diff --git a/openconcept/atmospherics/temperature_comp.py b/openconcept/atmospherics/temperature_comp.py index af509800..f30c4be3 100644 --- a/openconcept/atmospherics/temperature_comp.py +++ b/openconcept/atmospherics/temperature_comp.py @@ -46,8 +46,6 @@ def setup(self): self.declare_partials("fltcond|T", "fltcond|TempIncrement", rows=arange, cols=arange, val=1.0) def compute(self, inputs, outputs): - num_points = self.options["num_nodes"] - h_m = inputs["fltcond|h"] self.tropos_mask, self.strato_mask, self.smooth_mask = get_mask_arrays(h_m) @@ -56,8 +54,6 @@ def compute(self, inputs, outputs): outputs["fltcond|T"] = temp_K + inputs["fltcond|TempIncrement"] def compute_partials(self, inputs, partials): - num_points = self.options["num_nodes"] - h_m = inputs["fltcond|h"] derivs = compute_temp_derivs(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) diff --git a/openconcept/examples/B738.py b/openconcept/examples/B738.py index 4671f24b..ff9cab51 100644 --- a/openconcept/examples/B738.py +++ b/openconcept/examples/B738.py @@ -112,7 +112,7 @@ def setup(self): dv_comp.add_output_from_dict("ac|q_cruise") # Run a full mission analysis including takeoff, reserve_, cruise,reserve_ and descereserve_nt - analysis = self.add_subsystem( + self.add_subsystem( "analysis", MissionWithReserve(num_nodes=nn, aircraft_model=B738AirplaneModel), promotes_inputs=["*"], diff --git a/openconcept/examples/B738_aerostructural.py b/openconcept/examples/B738_aerostructural.py index ce61b4ad..f8f8bf97 100644 --- a/openconcept/examples/B738_aerostructural.py +++ b/openconcept/examples/B738_aerostructural.py @@ -50,7 +50,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - flight_phase = self.options["flight_phase"] # a propulsion system needs to be defined in order to provide thrust # information for the mission analysis code @@ -249,7 +248,7 @@ def setup(self): # ======================== Mission analysis ======================== # Run a full mission analysis including takeoff, reserve_, cruise,reserve_ and descereserve_nt - analysis = self.add_subsystem( + self.add_subsystem( "analysis", BasicMission(num_nodes=nn, aircraft_model=B738AirplaneModel), promotes_inputs=["*"], diff --git a/openconcept/examples/Caravan.py b/openconcept/examples/Caravan.py index 0ea3d534..1fa8af0c 100644 --- a/openconcept/examples/Caravan.py +++ b/openconcept/examples/Caravan.py @@ -7,7 +7,6 @@ from openconcept.examples.aircraft_data.caravan import data as acdata from openconcept.propulsion import TurbopropPropulsionSystem from openconcept.weights import SingleTurboPropEmptyWeight -from openconcept.costs import TurbopropOperatingCost from openconcept.aerodynamics import PolarDrag from openconcept.mission import FullMissionAnalysis @@ -129,7 +128,7 @@ def setup(self): dv_comp.add_output_from_dict("ac|num_passengers_max") dv_comp.add_output_from_dict("ac|q_cruise") - analysis = self.add_subsystem( + self.add_subsystem( "analysis", FullMissionAnalysis(num_nodes=nn, aircraft_model=CaravanAirplaneModel), promotes_inputs=["*"], diff --git a/openconcept/examples/HybridTwin_active_thermal.py b/openconcept/examples/HybridTwin_active_thermal.py index 9583e9b1..909d8b91 100644 --- a/openconcept/examples/HybridTwin_active_thermal.py +++ b/openconcept/examples/HybridTwin_active_thermal.py @@ -13,6 +13,7 @@ IndepVarComp, NewtonSolver, BoundsEnforceLS, + CaseReader, ) # imports for the airplane model itself @@ -76,7 +77,7 @@ def setup(self): else: controls.add_output("hybridization", val=1.0) - hybrid_factor = self.add_subsystem( + self.add_subsystem( "hybrid_factor", LinearInterpolator(num_nodes=nn), promotes_inputs=[("start_val", "hybridization"), ("end_val", "hybridization")], @@ -218,14 +219,14 @@ def setup(self): mission_data_comp.add_output("T_batt_initial", val=10.1, units="degC") # Ensure that any state variables are connected across the mission as intended - analysis = self.add_subsystem( + self.add_subsystem( "analysis", BasicMission(num_nodes=nn, aircraft_model=SeriesHybridTwinModel), promotes_inputs=["*"], promotes_outputs=["*"], ) - margins = self.add_subsystem( + self.add_subsystem( "margins", ExecComp( "MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload", @@ -243,7 +244,7 @@ def setup(self): self.connect("ac|weights|MTOW", "margins.MTOW") self.connect("ac|weights|W_battery", "margins.W_battery") - augobj = self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) + self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) self.connect("ac|weights|MTOW", "aug_obj.ac|weights|MTOW") self.connect("descent.fuel_used_final", "aug_obj.fuel_burn") diff --git a/openconcept/examples/HybridTwin_thermal.py b/openconcept/examples/HybridTwin_thermal.py index 5cd7f414..c161cffd 100644 --- a/openconcept/examples/HybridTwin_thermal.py +++ b/openconcept/examples/HybridTwin_thermal.py @@ -12,6 +12,7 @@ DirectSolver, IndepVarComp, NewtonSolver, + CaseReader, ) # imports for the airplane model itself @@ -68,7 +69,7 @@ def setup(self): else: controls.add_output("hybridization", val=1.0) - hybrid_factor = self.add_subsystem( + self.add_subsystem( "hybrid_factor", LinearInterpolator(num_nodes=nn), promotes_inputs=[("start_val", "hybridization"), ("end_val", "hybridization")], @@ -212,14 +213,14 @@ def setup(self): mission_data_comp.add_output("T_batt_initial", val=10.1, units="degC") # Ensure that any state variables are connected across the mission as intended - analysis = self.add_subsystem( + self.add_subsystem( "analysis", FullMissionAnalysis(num_nodes=nn, aircraft_model=SeriesHybridTwinModel), promotes_inputs=["*"], promotes_outputs=["*"], ) - margins = self.add_subsystem( + self.add_subsystem( "margins", ExecComp( "MTOW_margin = MTOW - OEW - total_fuel - W_battery - payload", @@ -237,7 +238,7 @@ def setup(self): self.connect("ac|weights|MTOW", "margins.MTOW") self.connect("ac|weights|W_battery", "margins.W_battery") - augobj = self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) + self.add_subsystem("aug_obj", AugmentedFBObjective(), promotes_outputs=["mixed_objective"]) self.connect("ac|weights|MTOW", "aug_obj.ac|weights|MTOW") self.connect("descent.fuel_used_final", "aug_obj.fuel_burn") diff --git a/openconcept/examples/KingAirC90GT.py b/openconcept/examples/KingAirC90GT.py index 64751592..d2eae2db 100644 --- a/openconcept/examples/KingAirC90GT.py +++ b/openconcept/examples/KingAirC90GT.py @@ -1,8 +1,6 @@ import numpy as np -from openmdao.api import Problem, Group, ScipyOptimizeDriver -from openmdao.api import DirectSolver, SqliteRecorder, IndepVarComp -from openmdao.api import NewtonSolver, BoundsEnforceLS +from openmdao.api import Problem, Group, DirectSolver, IndepVarComp, NewtonSolver # imports for the airplane model itself from openconcept.aerodynamics import PolarDrag @@ -132,7 +130,7 @@ def setup(self): dv_comp.add_output_from_dict("ac|num_engines") # Run a full mission analysis including takeoff, climb, cruise, and descent - analysis = self.add_subsystem( + self.add_subsystem( "analysis", FullMissionAnalysis(num_nodes=nn, aircraft_model=KingAirC90GTModel), promotes_inputs=["*"], diff --git a/openconcept/examples/N3_HybridSingleAisle_Refrig.py b/openconcept/examples/N3_HybridSingleAisle_Refrig.py index 678002bf..d4d44f0c 100644 --- a/openconcept/examples/N3_HybridSingleAisle_Refrig.py +++ b/openconcept/examples/N3_HybridSingleAisle_Refrig.py @@ -423,7 +423,7 @@ def setup(self): dv_comp.add_output_from_dict("ac|design_mission|TOW") # Run a full mission analysis including takeoff, reserve_, cruise,reserve_ and descereserve_nt - analysis = self.add_subsystem( + self.add_subsystem( "analysis", BasicMission(num_nodes=nn, aircraft_model=HybridSingleAisleModel, include_ground_roll=True), promotes_inputs=["*"], @@ -455,9 +455,8 @@ def set_values(prob, num_nodes): prob.set_val("cruise|h0", 35000.0, units="ft") prob.set_val("mission_range", 800, units="NM") prob.set_val("takeoff|v2", 160.0, units="kn") - nozzleprs = [0.85, 0.85, 0.71, 0.88] phases_list = ["groundroll", "climb", "cruise", "descent"] - for i, phase in enumerate(phases_list): + for phase in phases_list: prob.set_val(phase + ".hybrid_throttle_start", 0.00) prob.set_val(phase + ".hybrid_throttle_end", 0.00) prob.set_val(phase + ".fltcond|TempIncrement", 20, units="degC") diff --git a/openconcept/examples/aircraft_data/HybridSingleAisle.py b/openconcept/examples/aircraft_data/HybridSingleAisle.py index 89d20cc4..69bfed31 100644 --- a/openconcept/examples/aircraft_data/HybridSingleAisle.py +++ b/openconcept/examples/aircraft_data/HybridSingleAisle.py @@ -2,7 +2,6 @@ # Collected from various sources # including SOCATA pilot manual -import openmdao.api as om from openconcept.mission import IntegratorGroup from openconcept.thermal import PerfectHeatTransferComp from openconcept.utilities import ElementMultiplyDivideComp, AddSubtractComp diff --git a/openconcept/examples/tests/test_example_aircraft.py b/openconcept/examples/tests/test_example_aircraft.py index f3cb6765..5621b596 100644 --- a/openconcept/examples/tests/test_example_aircraft.py +++ b/openconcept/examples/tests/test_example_aircraft.py @@ -4,7 +4,6 @@ from openconcept.examples.B738 import run_738_analysis from openconcept.examples.TBM850 import run_tbm_analysis from openconcept.examples.HybridTwin_thermal import run_hybrid_twin_thermal_analysis -from openconcept.examples.HybridTwin_active_thermal import run_hybrid_twin_active_thermal_analysis from openconcept.examples.HybridTwin import run_hybrid_twin_analysis from openconcept.examples.Caravan import run_caravan_analysis from openconcept.examples.KingAirC90GT import run_kingair_analysis @@ -19,7 +18,7 @@ from openconcept.examples.B738_aerostructural import run_738_analysis as run_738Aerostruct_analysis OAS_installed = True -except: +except ImportError: OAS_installed = False diff --git a/openconcept/mission/phases.py b/openconcept/mission/phases.py index 6455a5de..2f6cf541 100644 --- a/openconcept/mission/phases.py +++ b/openconcept/mission/phases.py @@ -247,8 +247,6 @@ def setup(self): ) def compute(self, inputs, outputs): - - nn = self.options["num_nodes"] # compute the groundspeed on climb and desc inside = inputs["fltcond|Utrue"] ** 2 - inputs["fltcond|vs"] ** 2 groundspeed = np.sqrt(inside) @@ -331,7 +329,6 @@ def setup(self): ) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] m = inputs["weight"] floor_vec = np.where(np.less((GRAV_CONST - inputs["lift"] / m), 0.0), 0.0, 1.0) accel = ( @@ -407,7 +404,6 @@ def setup(self): ) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] cosg = inputs["fltcond|cosgamma"] sing = inputs["fltcond|singamma"] accel = ( @@ -831,7 +827,7 @@ def setup(self): promotes_outputs=["*"], ) self.add_subsystem("gs", Groundspeeds(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) - clcomp = self.add_subsystem( + self.add_subsystem( "clcomp", ElementMultiplyDivideComp( output_name="fltcond|CL", input_names=["CL_rotate_mult", "ac|aero|CLmax_TO"], vec_size=[nn, 1], length=1 diff --git a/openconcept/mission/tests/test_solver_phase_helpers.py b/openconcept/mission/tests/test_solver_phase_helpers.py index 7a77d8e5..8194b640 100644 --- a/openconcept/mission/tests/test_solver_phase_helpers.py +++ b/openconcept/mission/tests/test_solver_phase_helpers.py @@ -9,7 +9,6 @@ VerticalAcceleration, SteadyFlightCL, FlipVectorComp, - TakeoffTransition, ) from openconcept.utilities.constants import GRAV_CONST @@ -200,22 +199,6 @@ def test_partials(self): assert_check_partials(partials) -class HorizontalAccelerationTestCase_SteadyClimb(unittest.TestCase): - def setUp(self): - self.prob = Problem(HorizontalAccelerationTestGroup(num_nodes=9)) - self.prob.setup(check=True, force_alloc_complex=True) - self.prob["thrust"] = np.ones((9,)) * (100 + 100 * GRAV_CONST * 0.02) - self.prob["fltcond|singamma"] = np.ones((9,)) * 0.02 - self.prob.run_model() - - def test_steady_climb_flights(self): - assert_near_equal(self.prob["accel_horiz"], np.zeros((9,)), tolerance=1e-10) - - def test_partials(self): - partials = self.prob.check_partials(method="cs", out_stream=None) - assert_check_partials(partials) - - class HorizontalAccelerationTestCase_UnsteadyRunwayAccel(unittest.TestCase): def setUp(self): self.prob = Problem(HorizontalAccelerationTestGroup(num_nodes=9)) diff --git a/openconcept/mission/tests/test_trajectories.py b/openconcept/mission/tests/test_trajectories.py index 232220c4..fac098f8 100644 --- a/openconcept/mission/tests/test_trajectories.py +++ b/openconcept/mission/tests/test_trajectories.py @@ -152,8 +152,8 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - iv = self.add_subsystem("iv", om.IndepVarComp("x", val=np.linspace(0, 5, nn), units="s")) - ec = self.add_subsystem( + self.add_subsystem("iv", om.IndepVarComp("x", val=np.linspace(0, 5, nn), units="s")) + self.add_subsystem( "ec", om.ExecComp( ["df = -10.2*x**2 + 4.2*x -10.5"], @@ -203,7 +203,7 @@ class IntegratorTestMultipleOutputs(IntegratorGroupTestBase): def setup(self): super(IntegratorTestMultipleOutputs, self).setup() nn = self.options["num_nodes"] - ec2 = self.add_subsystem( + self.add_subsystem( "ec2", om.ExecComp( ["df2 = 5.1*x**2 +0.5*x-7.2"], @@ -257,7 +257,7 @@ class IntegratorTestPromotes(IntegratorGroupTestBase): def setup(self): super(IntegratorTestPromotes, self).setup() nn = self.options["num_nodes"] - ec2 = self.add_subsystem( + self.add_subsystem( "ec2", om.ExecComp( ["df2 = 5.1*x**2 +0.5*x-7.2"], @@ -311,7 +311,7 @@ class IntegratorTestValLimits(IntegratorGroupTestBase): def setup(self): super(IntegratorTestValLimits, self).setup() nn = self.options["num_nodes"] - ec2 = self.add_subsystem( + self.add_subsystem( "ec2", om.ExecComp( ["df2 = 5.1*x**2 +0.5*x-7.2"], @@ -370,7 +370,7 @@ class IntegratorTestDuplicateRateNames(IntegratorGroupTestBase): def setup(self): super(IntegratorTestDuplicateRateNames, self).setup() nn = self.options["num_nodes"] - ec2 = self.add_subsystem( + self.add_subsystem( "ec2", om.ExecComp( ["df = 5.1*x**3 +0.5*x-7.2"], @@ -397,7 +397,7 @@ def setUp(self): self.p = om.Problem(model=self.TestPhase(num_nodes=self.nn)) def test_asserts(self): - with self.assertRaises(ValueError) as cm: + with self.assertRaises(ValueError) as _: self.p.setup(force_alloc_complex=True) @@ -405,7 +405,7 @@ class IntegratorTestDuplicateStateNames(IntegratorGroupTestBase): def setup(self): super(IntegratorTestDuplicateStateNames, self).setup() nn = self.options["num_nodes"] - ec2 = self.add_subsystem( + self.add_subsystem( "ec2", om.ExecComp( ["df2 = 5.1*x**3 +0.5*x-7.2"], @@ -508,8 +508,8 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - iv = self.add_subsystem("iv", om.IndepVarComp("x", val=np.linspace(0, 5, nn), units="s")) - ec = self.add_subsystem( + self.add_subsystem("iv", om.IndepVarComp("x", val=np.linspace(0, 5, nn), units="s")) + self.add_subsystem( "ec", om.ExecComp( ["df = -10.2*x**2 + 4.2*x -10.5"], @@ -569,7 +569,7 @@ def setUp(self): self.p = om.Problem(model=phase) def test_raises_error(self): - with self.assertRaises(NameError) as x: + with self.assertRaises(NameError) as _: self.p.setup() @@ -578,8 +578,8 @@ def setUp(self): self.nn = 5 grp1 = IntegratorGroupTestBase(num_nodes=self.nn) grp2 = om.Group() - grp2a = grp2.add_subsystem("a", IntegratorGroupTestBase(num_nodes=self.nn)) - grp2b = grp2.add_subsystem("b", IntegratorGroupTestBase(num_nodes=self.nn)) + grp2.add_subsystem("a", IntegratorGroupTestBase(num_nodes=self.nn)) + grp2.add_subsystem("b", IntegratorGroupTestBase(num_nodes=self.nn)) phase = PhaseGroup(num_nodes=self.nn) phase.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) phase.add_subsystem("grp1", grp1) @@ -610,8 +610,8 @@ def setUp(self): self.nn = 5 grp1 = IntegratorGroupTestBase(num_nodes=self.nn) grp2 = om.Group() - grp2a = grp2.add_subsystem("a", IntegratorGroupTestBase(num_nodes=self.nn)) - grp2b = grp2.add_subsystem("b", IntegratorGroupTestBase(num_nodes=self.nn)) + grp2.add_subsystem("a", IntegratorGroupTestBase(num_nodes=self.nn)) + grp2.add_subsystem("b", IntegratorGroupTestBase(num_nodes=self.nn)) phase = PhaseGroup(num_nodes=self.nn) phase.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) phase.add_subsystem("grp1", grp1) @@ -650,8 +650,8 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) - a = self.add_subsystem("a", IntegratorGroupTestBase(num_nodes=nn)) - b = self.add_subsystem("b", IntegratorTestMultipleOutputs(num_nodes=nn)) + self.add_subsystem("a", IntegratorGroupTestBase(num_nodes=nn)) + self.add_subsystem("b", IntegratorTestMultipleOutputs(num_nodes=nn)) class PhaseForTrajTestWithPromotion(PhaseGroup): @@ -661,9 +661,9 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) - a = self.add_subsystem("a", IntegratorGroupTestBase(num_nodes=nn)) + self.add_subsystem("a", IntegratorGroupTestBase(num_nodes=nn)) # promote the outputs of b - b = self.add_subsystem( + self.add_subsystem( "b", IntegratorTestMultipleOutputs(num_nodes=nn), promotes_outputs=["*f2*"], promotes_inputs=["*df2"] ) @@ -675,11 +675,11 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) - a = self.add_subsystem( + self.add_subsystem( "a", IntegratorGroupTestBase(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] ) # promote the outputs of b - b = self.add_subsystem( + self.add_subsystem( "b", IntegratorTestMultipleOutputs(num_nodes=nn), promotes_outputs=["*f2*"], promotes_inputs=["*df2"] ) @@ -861,7 +861,7 @@ def setUp(self): phase1 = traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) phase2 = traj.add_subsystem("phase2", PhaseForTrajTest(num_nodes=5)) - phase3 = traj.add_subsystem("phase3", PhaseForTrajTest(num_nodes=5)) + traj.add_subsystem("phase3", PhaseForTrajTest(num_nodes=5)) traj.link_phases(phase1, phase2) @@ -932,9 +932,9 @@ def test_raises(self): self.nn = 5 traj = TrajectoryGroup() - phase1 = traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) + traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) - with self.assertRaises(ValueError) as context: + with self.assertRaises(ValueError) as _: traj.link_phases("phase1", "phase2", states_to_skip=["b.ode_integ.f"]) diff --git a/openconcept/thermal/battery_cooling.py b/openconcept/thermal/battery_cooling.py index f7c37337..34ecd63a 100644 --- a/openconcept/thermal/battery_cooling.py +++ b/openconcept/thermal/battery_cooling.py @@ -243,7 +243,6 @@ def setup(self): self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options["num_nodes"] n_cells = inputs["battery_weight"] * self.options["battery_weight_fraction"] / self.options["cell_mass"] n_bandoliers = n_cells / inputs["n_cpb"] / 2 @@ -266,10 +265,8 @@ def compute(self, inputs, outputs): mdot_b * cpf * (1 - np.exp(-NTU)) / 2 / inputs["n_cpb"] ) # divide out the total bandolier convection by 2 * n_cpb cells # the convective heat transfer is (Ts - Tin) * Kcell - PI = np.pi Tbar = inputs["T_battery"] - Rc = Dc / 2 K_cyl = 8 * np.pi * Hc * krc @@ -284,7 +281,7 @@ def compute(self, inputs, outputs): outputs["q"] = q_conv - qcheck = (Tbar - Ts) * K_cyl + # qcheck = (Tbar - Ts) * K_cyl # UAcomb = 1/(1/hconv/A_heat_trans+1/K_cyl/2/inputs['n_cpb']) # qcheck2 = (Tbar - inputs['T_in']) * mdot_b * cpf * (1 - np.exp(-UAcomb/mdot_b/cpf)) / 2 / inputs['n_cpb'] diff --git a/openconcept/thermal/chiller.py b/openconcept/thermal/chiller.py index ce2fc8d6..ad79ce50 100644 --- a/openconcept/thermal/chiller.py +++ b/openconcept/thermal/chiller.py @@ -129,7 +129,6 @@ def setup(self): self.declare_partials(["q_in_1", "q_in_2"], "power_rating", rows=np.arange(nn), cols=np.zeros(nn)) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] outputs["q_in_1"] = -inputs["COP"] * inputs["power_rating"] outputs["q_in_2"] = (inputs["COP"] + 1) * inputs["power_rating"] @@ -238,7 +237,7 @@ def setup(self): iv.add_output("bypass_start", val=1.0) iv.add_output("bypass_end", val=1.0) - li = self.add_subsystem( + self.add_subsystem( "li", LinearInterpolator(num_nodes=nn, units=None), promotes_outputs=[("vec", "bypass")] ) self.connect("control.bypass_start", "li.start_val") @@ -323,7 +322,6 @@ def setup(self): self.declare_partials(["COP"], ["T_c", "T_h", "eff_factor"], method="cs") def compute(self, inputs, outputs): - epsilon = 0.05 delta_T = inputs["T_h"] - inputs["T_c"] COP_raw = inputs["T_c"] / (delta_T) alpha = -1.5 diff --git a/openconcept/thermal/ducts.py b/openconcept/thermal/ducts.py index f96fa9ae..5d23b0ea 100644 --- a/openconcept/thermal/ducts.py +++ b/openconcept/thermal/ducts.py @@ -135,7 +135,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - gam = self.options["gamma"] self.add_input("Tt", shape=(nn,), units="K") self.add_input("M", shape=(nn,)) self.add_output("T", shape=(nn,), units="K") @@ -143,12 +142,10 @@ def setup(self): self.declare_partials(["T"], ["M", "Tt"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] gam = self.options["gamma"] outputs["T"] = inputs["Tt"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** -1 def compute_partials(self, inputs, J): - nn = self.options["num_nodes"] gam = self.options["gamma"] J["T", "Tt"] = (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** -1 J["T", "M"] = -inputs["Tt"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** -2 * (gam - 1) * inputs["M"] @@ -184,7 +181,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - gam = self.options["gamma"] self.add_input("T", shape=(nn,), units="K") self.add_input("M", shape=(nn,)) self.add_output("Tt", shape=(nn,), units="K") @@ -192,12 +188,10 @@ def setup(self): self.declare_partials(["Tt"], ["T", "M"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] gam = self.options["gamma"] outputs["Tt"] = inputs["T"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) def compute_partials(self, inputs, J): - nn = self.options["num_nodes"] gam = self.options["gamma"] J["Tt", "T"] = 1 + (gam - 1) / 2 * inputs["M"] ** 2 J["Tt", "M"] = inputs["T"] * (gam - 1) * inputs["M"] @@ -233,14 +227,12 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - gam = self.options["gamma"] self.add_input("pt", shape=(nn,), units="Pa") self.add_input("M", shape=(nn,)) self.add_output("p", shape=(nn,), units="Pa") self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options["num_nodes"] gam = self.options["gamma"] outputs["p"] = inputs["pt"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** (-gam / (gam - 1)) @@ -275,7 +267,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - gam = self.options["gamma"] self.add_input("p", shape=(nn,), units="Pa") self.add_input("M", shape=(nn,)) self.add_output("pt", shape=(nn,), units="Pa") @@ -283,12 +274,10 @@ def setup(self): self.declare_partials(["pt"], ["p", "M"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] gam = self.options["gamma"] outputs["pt"] = inputs["p"] * (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** (gam / (gam - 1)) def compute_partials(self, inputs, J): - nn = self.options["num_nodes"] gam = self.options["gamma"] J["pt", "p"] = (1 + (gam - 1) / 2 * inputs["M"] ** 2) ** (gam / (gam - 1)) J["pt", "M"] = ( @@ -331,14 +320,12 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - R = self.options["R"] self.add_input("p", shape=(nn,), units="Pa") self.add_input("T", shape=(nn,), units="K") self.add_output("rho", shape=(nn,), units="kg/m**3") self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options["num_nodes"] R = self.options["R"] outputs["rho"] = inputs["p"] / R / inputs["T"] @@ -380,7 +367,6 @@ def setup(self): self.declare_partials(["a"], ["T"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] R = self.options["R"] gam = self.options["gamma"] T = inputs["T"].copy() @@ -428,7 +414,6 @@ def setup(self): self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options["num_nodes"] outputs["M"] = inputs["Utrue"] / inputs["a"] @@ -495,7 +480,6 @@ def setup(self): self.declare_partials(["pt_out"], ["area", "dynamic_pressure_loss_factor"], rows=arange, cols=np.zeros((nn,))) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] dynamic_pressure = 0.5 * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 2 if np.min(inputs["mdot"]) <= 0.0: @@ -515,16 +499,8 @@ def compute(self, inputs, outputs): outputs["Tt_out"] = tt_out def compute_partials(self, inputs, J): - dynamic_pressure = 0.5 * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 2 - tt_out = inputs["Tt_in"] + inputs["heat_in"] / inputs["cp"] / inputs["mdot"] - # bool_array_tt = np.where(tt_out <= 0.0, np.zeros(inputs['Tt_in'].shape), np.ones(inputs['Tt_in'].shape)) - pt_out = ( - inputs["pt_in"] * inputs["pressure_recovery"] - - dynamic_pressure * inputs["dynamic_pressure_loss_factor"] - + inputs["delta_p"] - ) - # bool_array_pt = np.where(pt_out <= 0.0, np.zeros(inputs['pt_in'].shape), np.ones(inputs['pt_in'].shape)) nn = self.options["num_nodes"] + J["Tt_out", "Tt_in"] = np.ones((nn,)) J["Tt_out", "heat_in"] = 1 / inputs["cp"] / inputs["mdot"] J["Tt_out", "cp"] = -inputs["heat_in"] / inputs["cp"] ** 2 / inputs["mdot"] @@ -698,7 +674,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - gam = self.options["gamma"] self.add_input("nozzle_pressure_ratio", shape=(nn,)) self.add_output("M", shape=(nn,)) self.declare_partials(["*"], ["*"], method="cs") @@ -763,7 +738,6 @@ def setup(self): self.declare_partials(["*"], ["*"], method="cs") def compute(self, inputs, outputs): - nn = self.options["num_nodes"] cfg = self.options["cfg"] outputs["F_net"] = inputs["mdot"] * ( inputs["mdot"] / inputs["area_nozzle"] / inputs["rho_nozzle"] * cfg - inputs["Utrue_inf"] @@ -915,7 +889,6 @@ def setup(self): self.declare_partials(["pt"], ["dynamic_pressure_loss_factor"], rows=arange, cols=np.zeros((nn,))) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] dynamic_pressure = 0.5 * inputs["mdot"] ** 2 / inputs["rho"] / inputs["area"] ** 2 pt_out = inputs["pt_in"] - dynamic_pressure * inputs["dynamic_pressure_loss_factor"] diff --git a/openconcept/thermal/heat_exchanger.py b/openconcept/thermal/heat_exchanger.py index be59f07f..e22a7502 100644 --- a/openconcept/thermal/heat_exchanger.py +++ b/openconcept/thermal/heat_exchanger.py @@ -1088,13 +1088,11 @@ def setup(self): self.declare_partials(["effectiveness"], ["NTU", "C_ratio"], rows=arange, cols=arange) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] Cr = inputs["C_ratio"] ntu = inputs["NTU"] outputs["effectiveness"] = 1 - np.exp(ntu**0.22 / Cr * (np.exp(-Cr * ntu**0.78) - 1)) def compute_partials(self, inputs, J): - nn = self.options["num_nodes"] Cr = inputs["C_ratio"] ntu = inputs["NTU"] J["effectiveness", "C_ratio"] = -np.exp((ntu**0.22 * (np.exp(-Cr * ntu**0.78) - 1)) / Cr) * ( @@ -1324,8 +1322,6 @@ def compute(self, inputs, outputs): dyn_press_hot = (1 / 2) * (inputs["mdot_hot"] / inputs["xs_area_hot"]) ** 2 / inputs["rho_hot"] Kec = self.options["Ke_cold"] Kcc = self.options["Kc_cold"] - Keh = self.options["Ke_hot"] - Kch = self.options["Kc_hot"] outputs["delta_p_cold"] = dyn_press_cold * ( -Kec - Kcc - 4 * inputs["length_overall"] * inputs["f_cold"] / inputs["dh_cold"] ) @@ -1338,8 +1334,6 @@ def compute_partials(self, inputs, J): dyn_press_hot = (1 / 2) * (inputs["mdot_hot"] / inputs["xs_area_hot"]) ** 2 / inputs["rho_hot"] Kec = self.options["Ke_cold"] Kcc = self.options["Kc_cold"] - Keh = self.options["Ke_hot"] - Kch = self.options["Kc_hot"] losses_cold = -Kec - Kcc - 4 * inputs["length_overall"] * inputs["f_cold"] / inputs["dh_cold"] losses_hot = -Kec - Kcc - 4 * inputs["width_overall"] * inputs["f_hot"] / inputs["dh_hot"] diff --git a/openconcept/thermal/heat_pipe.py b/openconcept/thermal/heat_pipe.py index 406a8350..1d3a7dee 100644 --- a/openconcept/thermal/heat_pipe.py +++ b/openconcept/thermal/heat_pipe.py @@ -249,7 +249,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - arange = np.arange(0, nn) self.add_input("inner_diam", units="m", val=0.02) self.add_input("wall_thickness", val=1.25e-3, units="m") diff --git a/openconcept/thermal/hose.py b/openconcept/thermal/hose.py index 1691b04a..857d9dee 100644 --- a/openconcept/thermal/hose.py +++ b/openconcept/thermal/hose.py @@ -75,7 +75,6 @@ def _compute_pressure_drop(self, inputs): return dp def compute(self, inputs, outputs): - nn = self.options["num_nodes"] sigma = self.options["hose_operating_stress"] rho_hose = self.options["hose_density"] @@ -88,7 +87,6 @@ def compute(self, inputs, outputs): outputs["component_weight"] = w_hose + w_coolant def compute_partials(self, inputs, J): - nn = self.options["num_nodes"] sigma = self.options["hose_operating_stress"] rho_hose = self.options["hose_density"] thickness = inputs["hose_diameter"] * inputs["hose_design_pressure"] / 2 / sigma @@ -113,7 +111,6 @@ def compute_partials(self, inputs, J): # use a colored complex step approach cs_step = 1e-30 - dp_base = self._compute_pressure_drop(inputs) cs_inp_list = ["rho_coolant", "mdot_coolant", "hose_diameter", "hose_length", "mu_coolant"] fake_inputs = dict() diff --git a/openconcept/thermal/pump.py b/openconcept/thermal/pump.py index ea2e5196..664cfc66 100644 --- a/openconcept/thermal/pump.py +++ b/openconcept/thermal/pump.py @@ -48,7 +48,6 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] - eta = self.options["efficiency"] weight_inc = self.options["weight_inc"] self.add_input("power_rating", units="W", desc="Pump electrical power rating") @@ -70,7 +69,6 @@ def setup(self): self.declare_partials(["component_weight"], ["power_rating"], val=weight_inc) def compute(self, inputs, outputs): - nn = self.options["num_nodes"] eta = self.options["efficiency"] weight_inc = self.options["weight_inc"] weight_base = self.options["weight_base"] @@ -84,7 +82,6 @@ def compute(self, inputs, outputs): outputs["component_sizing_margin"] = outputs["elec_load"] / inputs["power_rating"] def compute_partials(self, inputs, J): - nn = self.options["num_nodes"] eta = self.options["efficiency"] J["elec_load", "mdot_coolant"] = inputs["delta_p"] / inputs["rho_coolant"] / eta diff --git a/openconcept/thermal/tests/test_battery_cooling.py b/openconcept/thermal/tests/test_battery_cooling.py index dda5a756..e25a9af3 100644 --- a/openconcept/thermal/tests/test_battery_cooling.py +++ b/openconcept/thermal/tests/test_battery_cooling.py @@ -1,7 +1,7 @@ import unittest import numpy as np from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -from openmdao.api import IndepVarComp, Group, Problem, DirectSolver, NewtonSolver, IndepVarComp +from openmdao.api import IndepVarComp, Group, Problem, DirectSolver, NewtonSolver from openconcept.thermal import LiquidCooledBattery @@ -137,7 +137,6 @@ def generate_model(self, nn): An example demonstrating unsteady battery cooling """ from openconcept.mission import PhaseGroup, TrajectoryGroup - import openmdao.api as om import numpy as np class VehicleModel(Group): diff --git a/openconcept/thermal/tests/test_ducts.py b/openconcept/thermal/tests/test_ducts.py index 6a4339e6..5d06a072 100644 --- a/openconcept/thermal/tests/test_ducts.py +++ b/openconcept/thermal/tests/test_ducts.py @@ -1,6 +1,5 @@ import unittest -import numpy as np -from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials +from openmdao.utils.assert_utils import assert_near_equal from openconcept.thermal import ImplicitCompressibleDuct_ExternalHX import openmdao.api as om import warnings @@ -29,7 +28,6 @@ def setup(self): else: self.options["thermo_method"] = "CEA" self.options["thermo_data"] = pyc.species_data.janaf - FUEL_TYPE = "JP-7" self.add_subsystem("fc", pyc.FlightConditions()) # ram_recovery | ram_recovery @@ -120,7 +118,7 @@ def setup(self): self.options["thermo_method"] = "CEA" self.options["thermo_data"] = pyc.species_data.janaf - design = self.pyc_add_pnt("design", PyCycleDuct(design=True, thermo_method="CEA")) + self.pyc_add_pnt("design", PyCycleDuct(design=True, thermo_method="CEA")) # define the off-design conditions we want to run self.od_pts = [] @@ -190,7 +188,7 @@ def check_params_match_pycycle(prob, list_output=True, case_name=""): HAS_PYCYCLE = False -except: +except ImportError: HAS_PYCYCLE = False diff --git a/openconcept/thermal/tests/test_heat_exchanger.py b/openconcept/thermal/tests/test_heat_exchanger.py index 83fb28ea..c25fb16c 100644 --- a/openconcept/thermal/tests/test_heat_exchanger.py +++ b/openconcept/thermal/tests/test_heat_exchanger.py @@ -318,12 +318,12 @@ def test_by_hand(self): hot_one_cell_width = t_f + w_h n_hot_wide = cold_length / hot_one_cell_width - #######____ + # ######____ # | | | # | | | # | | | # |____| |___ - ########## + # ######### height_overall = n_cold_tall * (cold_one_layer_height + hot_one_layer_height) # note does not include case diff --git a/openconcept/thermal/tests/test_heat_pipe.py b/openconcept/thermal/tests/test_heat_pipe.py index e16c7829..83667dff 100644 --- a/openconcept/thermal/tests/test_heat_pipe.py +++ b/openconcept/thermal/tests/test_heat_pipe.py @@ -69,7 +69,6 @@ def test_simple_vector(self): def test_two_pipes(self): nn = 3 - prob = Problem() # Run one and two pipes to compare results one = Problem() diff --git a/openconcept/thermal/tests/test_manifold.py b/openconcept/thermal/tests/test_manifold.py index a3d5cd10..48829f17 100644 --- a/openconcept/thermal/tests/test_manifold.py +++ b/openconcept/thermal/tests/test_manifold.py @@ -1,7 +1,7 @@ import unittest import numpy as np from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials -from openmdao.api import IndepVarComp, Group, Problem +from openmdao.api import Problem from openconcept.thermal import FlowSplit, FlowCombine diff --git a/openconcept/thermal/tests/test_pump.py b/openconcept/thermal/tests/test_pump.py index 3cedca70..65cef234 100644 --- a/openconcept/thermal/tests/test_pump.py +++ b/openconcept/thermal/tests/test_pump.py @@ -43,7 +43,7 @@ def test_scalar(self): partials = prob.check_partials(method="cs", compact_print=True) assert_check_partials(partials) - def test_scalar(self): + def test_vector(self): prob, elec_load, weight, margin = self.generate_model(nn=11) prob.run_model() assert_near_equal(prob.get_val("pump.elec_load", units="W"), elec_load, tolerance=1e-10) diff --git a/openconcept/thermal/thermal.py b/openconcept/thermal/thermal.py index 554accc6..df6b28e2 100644 --- a/openconcept/thermal/thermal.py +++ b/openconcept/thermal/thermal.py @@ -118,7 +118,6 @@ def compute(self, inputs, outputs): outputs["dTdt"] = (inputs["q_in"] - inputs["q_out"]) / inputs["mass"] / spec_heat def compute_partials(self, inputs, J): - nn_tot = self.options["num_nodes"] spec_heat = self.options["specific_heat"] J["dTdt", "mass"] = -(inputs["q_in"] - inputs["q_out"]) / inputs["mass"] ** 2 / spec_heat @@ -219,7 +218,6 @@ def initialize(self): def setup(self): nn_tot = self.options["num_nodes"] - arange = np.arange(0, nn_tot) self.add_input("T_in", units="K", shape=(nn_tot,)) self.add_input("T_surface", units="K", shape=(nn_tot,)) diff --git a/openconcept/utilities/math/add_subtract_comp.py b/openconcept/utilities/math/add_subtract_comp.py index 89c602d6..ba5aea0e 100644 --- a/openconcept/utilities/math/add_subtract_comp.py +++ b/openconcept/utilities/math/add_subtract_comp.py @@ -250,7 +250,7 @@ def compute(self, inputs, outputs): outputs : Vector unscaled, dimensional output variables read via outputs[key] """ - for (output_name, input_names, vec_size, length, val, scaling_factors, kwargs) in self._add_systems: + for (output_name, input_names, vec_size, length, _, scaling_factors, _) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] diff --git a/openconcept/utilities/math/combine_split_comp.py b/openconcept/utilities/math/combine_split_comp.py index e6948efa..41fbd75c 100644 --- a/openconcept/utilities/math/combine_split_comp.py +++ b/openconcept/utilities/math/combine_split_comp.py @@ -205,7 +205,7 @@ def compute(self, inputs, outputs): outputs : Vector unscaled, dimensional output variables read via outputs[key] """ - for (output_name, input_names, vec_sizes, length, val, kwargs) in self._add_systems: + for (output_name, input_names, _, length, _, _) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] @@ -218,7 +218,7 @@ def compute(self, inputs, outputs): else: temp = np.empty([0, length], dtype=np.dtype) - for i, input_name in enumerate(input_names): + for input_name in input_names: temp = np.concatenate((temp, inputs[input_name])) outputs[output_name] = temp @@ -423,15 +423,10 @@ def compute(self, inputs, outputs): outputs : Vector unscaled, dimensional output variables read via outputs[key] """ - for (output_names, input_name, vec_sizes, length, val, kwargs) in self._add_systems: + for (output_names, input_name, vec_sizes, length, _, _) in self._add_systems: if isinstance(output_names, str): output_names = [output_names] - if self.under_complex_step: - dtype = np.complex_ - else: - dtype = np.float64 - for i, output_name in enumerate(output_names): if i == 0: start_idx = 0 diff --git a/openconcept/utilities/math/derivatives.py b/openconcept/utilities/math/derivatives.py index 62cd2352..7da7d134 100644 --- a/openconcept/utilities/math/derivatives.py +++ b/openconcept/utilities/math/derivatives.py @@ -89,7 +89,6 @@ def first_deriv(dts, q, n_segments=1, n_simpson_intervals_per_segment=2, order=4 """ n_int_seg = n_simpson_intervals_per_segment - n_int_tot = n_segments * n_int_seg nn_seg = n_simpson_intervals_per_segment * 2 + 1 nn_tot = n_segments * nn_seg if order == 4 and n_int_seg < 2: @@ -155,7 +154,6 @@ def first_deriv_partials(dts, q, n_segments=1, n_simpson_intervals_per_segment=2 returned in CSR format as rowidxs[i], colidxs[i], data[i] """ n_int_seg = n_simpson_intervals_per_segment - n_int_tot = n_segments * n_int_seg nn_seg = n_simpson_intervals_per_segment * 2 + 1 nn_tot = n_segments * nn_seg if order == 4 and n_int_seg < 2: @@ -167,7 +165,6 @@ def first_deriv_partials(dts, q, n_segments=1, n_simpson_intervals_per_segment=2 if len(dts) != n_segments: raise ValueError("must provide same number of dts as segments") - dqdt = np.zeros(q.shape) if order == 4: stencil_vec, rowidx, colidx = first_deriv_fourth_order_accurate_stencil(nn_seg) elif order == 2: @@ -197,10 +194,6 @@ def first_deriv_partials(dts, q, n_segments=1, n_simpson_intervals_per_segment=2 # next compute the indices and values of dq' / d(dt[i]) in CSR format # the dimension of the matrix for all segments is nn_tot x 1 - if order == 2: - n_to_tile = 3 - else: - n_to_tile = 5 rowidxs_wrt_dt.append(np.arange(0, nn_seg) + i * nn_seg) colidxs_wrt_dt.append(np.zeros((nn_seg,), dtype=np.int32)) local_partials = -np.dot(stencil_mat, q[i * nn_seg : (i + 1) * nn_seg]) * dt_seg**-2 @@ -309,21 +302,17 @@ def setup(self): def compute(self, inputs, outputs): segment_names = self.options["segment_names"] - quantity_units = self.options["quantity_units"] - diff_units = self.options["diff_units"] order = self.options["order"] n_int_per_seg = self.options["num_intervals"] - nn_seg = n_int_per_seg * 2 + 1 if segment_names is None: n_segments = 1 dts = [inputs["dt"][0]] else: n_segments = len(segment_names) dts = [] - for i_seg, segment_name in enumerate(segment_names): + for segment_name in segment_names: input_name = segment_name + "|dt" dts.append(inputs[input_name][0]) - nn_tot = nn_seg * n_segments dqdt = first_deriv( dts, inputs["q"], n_segments=n_segments, n_simpson_intervals_per_segment=n_int_per_seg, order=order ) @@ -331,22 +320,18 @@ def compute(self, inputs, outputs): def compute_partials(self, inputs, J): segment_names = self.options["segment_names"] - quantity_units = self.options["quantity_units"] - diff_units = self.options["diff_units"] order = self.options["order"] n_int_per_seg = self.options["num_intervals"] - nn_seg = n_int_per_seg * 2 + 1 if segment_names is None: n_segments = 1 dts = [inputs["dt"][0]] else: n_segments = len(segment_names) dts = [] - for i_seg, segment_name in enumerate(segment_names): + for segment_name in segment_names: input_name = segment_name + "|dt" dts.append(inputs[input_name][0]) - nn_tot = nn_seg * n_segments wrt_q, wrt_dt = first_deriv_partials( dts, inputs["q"], n_segments=n_segments, n_simpson_intervals_per_segment=n_int_per_seg, order=order ) diff --git a/openconcept/utilities/math/integrals.py b/openconcept/utilities/math/integrals.py index 4db8253b..83d4b72f 100644 --- a/openconcept/utilities/math/integrals.py +++ b/openconcept/utilities/math/integrals.py @@ -754,7 +754,7 @@ def compute(self, inputs, outputs): delta_t = inputs["t_final"] - inputs["t_initial"] dts = [delta_t[0] / (num_nodes - 1)] - for name, options in self._state_vars.items(): + for _, options in self._state_vars.items(): if options["zero_start"]: q0 = np.array([0.0]) else: @@ -795,7 +795,7 @@ def compute_partials(self, inputs, J): delta_t = inputs["t_final"] - inputs["t_initial"] dts = [delta_t[0] / (num_nodes - 1)] - for name, options in self._state_vars.items(): + for _, options in self._state_vars.items(): start_name = options["start_name"] end_name = options["end_name"] qty_name = options["name"] @@ -1077,7 +1077,6 @@ def compute(self, inputs, outputs): single_point = False if segment_names is None: - n_segments = 1 if time_setup == "dt": dts = [inputs["dt"][0]] elif time_setup == "duration": @@ -1089,9 +1088,8 @@ def compute(self, inputs, outputs): delta_t = inputs["t_final"] - inputs["t_initial"] dts = [delta_t[0] / (num_nodes - 1)] else: - n_segments = len(segment_names) dts = [] - for i_seg, segment_name in enumerate(segment_names): + for segment_name in segment_names: input_name = segment_name + "|dt" dts.append(inputs[input_name][0]) if zero_start: @@ -1119,8 +1117,6 @@ def compute(self, inputs, outputs): def compute_partials(self, inputs, J): segment_names = self.options["segment_names"] - quantity_units = self.options["quantity_units"] - diff_units = self.options["diff_units"] num_nodes = self.options["num_nodes"] segments_to_count = self.options["segments_to_count"] zero_start = self.options["zero_start"] @@ -1133,13 +1129,6 @@ def compute_partials(self, inputs, J): single_point = False if not single_point: if segment_names is None: - n_segments = 1 - else: - n_segments = len(segment_names) - nn_tot = num_nodes * n_segments - - if segment_names is None: - n_segments = 1 if time_setup == "dt": dts = [inputs["dt"][0]] elif time_setup == "duration": @@ -1148,9 +1137,8 @@ def compute_partials(self, inputs, J): delta_t = inputs["t_final"] - inputs["t_initial"] dts = [delta_t[0] / (num_nodes - 1)] else: - n_segments = len(segment_names) dts = [] - for i_seg, segment_name in enumerate(segment_names): + for segment_name in segment_names: input_name = segment_name + "|dt" dts.append(inputs[input_name][0]) diff --git a/openconcept/utilities/math/multiply_divide_comp.py b/openconcept/utilities/math/multiply_divide_comp.py index f41e125e..77f9fb9c 100644 --- a/openconcept/utilities/math/multiply_divide_comp.py +++ b/openconcept/utilities/math/multiply_divide_comp.py @@ -211,7 +211,7 @@ def setup(self): vec_size, length, val, - scaling_factor, + _, divide, input_units, kwargs, @@ -305,17 +305,17 @@ def compute(self, inputs, outputs): input_names, vec_size, length, - val, + _, scaling_factor, divide, - input_units, - kwargs, + _, + _, ) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] if divide is None: - divide = [False for k in range(len(input_names))] + divide = [False for _ in range(len(input_names))] if isinstance(vec_size, Iterable): # scalar - vector mutliplication @@ -347,11 +347,11 @@ def compute_partials(self, inputs, J): input_names, vec_size, length, - val, + _, scaling_factor, divide, - input_units, - kwargs, + _, + _, ) in self._add_systems: if isinstance(input_names, str): input_names = [input_names] @@ -369,7 +369,7 @@ def compute_partials(self, inputs, J): else: shape = (vec_out_size, length) - for j, input_name in enumerate(input_names): + for input_name in input_names: temp = np.ones(shape) for i, input_name_partial in enumerate(input_names): if input_name_partial != input_name: diff --git a/openconcept/utilities/math/tests/test_combine_split.py b/openconcept/utilities/math/tests/test_combine_split.py index 7a8abb1c..a93da836 100644 --- a/openconcept/utilities/math/tests/test_combine_split.py +++ b/openconcept/utilities/math/tests/test_combine_split.py @@ -134,7 +134,7 @@ def setUp(self): self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["a", "b"]) - combiner = self.p.model.add_subsystem( + self.p.model.add_subsystem( name="vector_concat_comp", subsys=VectorConcatenateComp( "concat_output", ["input_a", "input_b"], vec_sizes=[self.nn, self.nn], length=self.length @@ -473,7 +473,7 @@ def setUp(self): self.p.model.add_subsystem(name="ivc", subsys=ivc, promotes_outputs=["input_to_split"]) - splitter = self.p.model.add_subsystem( + self.p.model.add_subsystem( name="vector_split_comp", subsys=VectorSplitComp(["output_a", "output_b"], "input_to_split", vec_sizes=[self.nn, self.nn], length=3), ) diff --git a/openconcept/utilities/math/tests/test_integrals.py b/openconcept/utilities/math/tests/test_integrals.py index 28748bd2..46e4bef1 100644 --- a/openconcept/utilities/math/tests/test_integrals.py +++ b/openconcept/utilities/math/tests/test_integrals.py @@ -337,11 +337,6 @@ def test_quadratic_multiple_integrands(self): assert_check_partials(partials, atol=1e-8, rtol=1e0) def test_quadratic_both_units_correct(self): - num_nodes = self.num_nodes - nn_tot = num_nodes - x = np.linspace(0, nn_tot - 1, nn_tot) - fprime = 4 * x**2 - 8 * x + 5 - f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x prob = Problem( IntegratorTestGroup( num_nodes=self.num_nodes, @@ -511,10 +506,6 @@ def __init__(self, *args, **kwargs): class EdgeCaseTestCases(unittest.TestCase): def test_quadratic_even_num_nodes(self): num_nodes = 10 - nn_tot = num_nodes - x = np.linspace(0, nn_tot - 1, nn_tot) - fprime = 4 * x**2 - 8 * x + 5 - f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x with self.assertRaises(ValueError) as cm: prob = Problem( IntegratorTestGroup(num_nodes=num_nodes, integrator="simpson", rate_units="kg/s", diff_units="s") @@ -529,7 +520,6 @@ def test_default_value_scalar(self): nn_tot = num_nodes x = np.linspace(0, nn_tot - 1, nn_tot) fprime = 4 * x**2 - 8 * x + 5 - f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x prob = Problem( IntegratorTestGroup( @@ -554,7 +544,6 @@ def test_default_value_vector(self): nn_tot = num_nodes x = np.linspace(0, nn_tot - 1, nn_tot) fprime = 4 * x**2 - 8 * x + 5 - f = 4 * x**3 / 3 - 8 * x**2 / 2 + 5 * x prob = Problem( IntegratorTestGroup( diff --git a/openconcept/utilities/math/tests/test_math.py b/openconcept/utilities/math/tests/test_math.py index 3626600b..02fc30bf 100644 --- a/openconcept/utilities/math/tests/test_math.py +++ b/openconcept/utilities/math/tests/test_math.py @@ -164,7 +164,6 @@ def test_quadratic_multi_phase_units(self): prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 5 nn_seg = n_int_per_seg * 2 + 1 - nn_tot = (n_int_per_seg * 2 + 1) * 3 x = np.concatenate([np.linspace(0, 2, nn_seg), np.linspace(2, 3, nn_seg), np.linspace(3, 6, nn_seg)]) f_test = 5 * x**2 + 7 * x - 3 fp_exact = 10 * x + 7 @@ -190,7 +189,6 @@ def test_quadratic_multi_phase_units_7int(self): prob.setup(check=True, force_alloc_complex=True) n_int_per_seg = 7 nn_seg = n_int_per_seg * 2 + 1 - nn_tot = (n_int_per_seg * 2 + 1) * 3 x = np.concatenate([np.linspace(0, 2, nn_seg), np.linspace(2, 3, nn_seg), np.linspace(3, 6, nn_seg)]) f_test = 5 * x**2 + 7 * x - 3 fp_exact = 10 * x + 7 diff --git a/openconcept/utilities/math/tests/test_multiply_divide_comp.py b/openconcept/utilities/math/tests/test_multiply_divide_comp.py index cb623fde..c9cd4221 100644 --- a/openconcept/utilities/math/tests/test_multiply_divide_comp.py +++ b/openconcept/utilities/math/tests/test_multiply_divide_comp.py @@ -1,7 +1,6 @@ from __future__ import print_function, division, absolute_import import unittest -import pytest import numpy as np from openmdao.api import Problem, Group, IndepVarComp diff --git a/openconcept/utilities/selector.py b/openconcept/utilities/selector.py index f3107497..925ee12c 100644 --- a/openconcept/utilities/selector.py +++ b/openconcept/utilities/selector.py @@ -82,8 +82,6 @@ def compute(self, inputs, outputs): def compute_partials(self, inputs, J): input_names = list(self.options["input_names"]) - num_inputs = len(input_names) - nn = self.options["num_nodes"] selector = np.around(inputs["selector"]) diff --git a/openconcept/utilities/tests/test_dict_indepvarcomp.py b/openconcept/utilities/tests/test_dict_indepvarcomp.py index 4855fc83..b19af486 100644 --- a/openconcept/utilities/tests/test_dict_indepvarcomp.py +++ b/openconcept/utilities/tests/test_dict_indepvarcomp.py @@ -51,8 +51,6 @@ def setUp(self): self.p.run_model() def test_results(self): - a = self.p["a"] - b = self.p["b"] out = self.p["geom|S_ref"] expected = 20 assert_near_equal(out, expected, 1e-16) diff --git a/openconcept/utilities/tests/test_dvlabel.py b/openconcept/utilities/tests/test_dvlabel.py index dcdd9cdf..14e80cd9 100644 --- a/openconcept/utilities/tests/test_dvlabel.py +++ b/openconcept/utilities/tests/test_dvlabel.py @@ -1,7 +1,6 @@ from __future__ import print_function, division, absolute_import import unittest -import imp import numpy as np from openmdao.api import Problem, Group, IndepVarComp diff --git a/openconcept/utilities/visualization.py b/openconcept/utilities/visualization.py index c2ed10f1..73795896 100644 --- a/openconcept/utilities/visualization.py +++ b/openconcept/utilities/visualization.py @@ -1,6 +1,6 @@ try: from matplotlib import pyplot as plt -except: +except ImportError: # don't want a matplotlib dependency on Travis/Appveyor pass import numpy as np @@ -70,7 +70,6 @@ def plot_trajectory_grid( if file_counter >= 0: # write the file if savefig is not None: - fig.tight_layout() plt.savefig(savefig + "_" + str(file_counter) + ".pdf") fig, axs = plt.subplots(grid_layout[0], grid_layout[1], sharex=True, figsize=figsize) file_counter += 1 diff --git a/openconcept/weights/weights_turboprop.py b/openconcept/weights/weights_turboprop.py index bc8a97af..d918bda0 100644 --- a/openconcept/weights/weights_turboprop.py +++ b/openconcept/weights/weights_turboprop.py @@ -4,7 +4,7 @@ from openconcept.utilities import AddSubtractComp, ElementMultiplyDivideComp import math -##TODO: add fuel system weight back in (depends on Wf, which depends on MTOW and We, and We depends on fuel system weight) +# TODO: add fuel system weight back in (depends on Wf, which depends on MTOW and We, and We depends on fuel system weight) class WingWeight_SmallTurboprop(ExplicitComponent): @@ -396,13 +396,11 @@ def setup(self): self.declare_partials(["W_nacelle"], ["*"]) def compute(self, inputs, outputs): - n_ult = self.options["n_ult"] # Torenbeek method, Roskam PVC5pg78eq5.30 W_nacelle = 2.5 * inputs["P_TO"] ** 0.5 outputs["W_nacelle"] = W_nacelle def compute_partials(self, inputs, J): - n_ult = self.options["n_ult"] # Torenbeek method, Roskam PVC5pg78eq5.30 J["W_nacelle", "P_TO"] = 0.5 * 2.5 * inputs["P_TO"] ** (0.5 - 1) @@ -428,13 +426,11 @@ def setup(self): self.declare_partials(["W_nacelle"], ["*"]) def compute(self, inputs, outputs): - n_ult = self.options["n_ult"] # Torenbeek method, Roskam PVC5pg78eq5.33 W_nacelle = 0.14 * inputs["P_TO"] outputs["W_nacelle"] = W_nacelle def compute_partials(self, inputs, J): - n_ult = self.options["n_ult"] # Torenbeek method, Roskam PVC5pg78eq5.30 J["W_nacelle", "P_TO"] = 0.14 @@ -693,7 +689,7 @@ def setup(self): if __name__ == "__main__": - from openmdao.api import IndepVarComp, Problem + from openmdao.api import Problem prob = Problem() prob.model = Group() From adf1eae99fc85f0f0c1cd6542c62b3cadc4bd4aa Mon Sep 17 00:00:00 2001 From: eytanadler Date: Thu, 11 Aug 2022 10:46:17 -0400 Subject: [PATCH 4/7] Ran black on linted code --- .../openaerostruct/tests/test_aerostructural.py | 8 +++++++- .../openaerostruct/tests/test_drag_polar.py | 8 +++++++- openconcept/examples/HybridTwin.py | 12 +++++++++++- openconcept/mission/tests/test_trajectories.py | 4 +--- openconcept/thermal/chiller.py | 4 +--- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py b/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py index c32eb0f6..07e98c10 100644 --- a/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py +++ b/openconcept/aerodynamics/openaerostruct/tests/test_aerostructural.py @@ -5,7 +5,13 @@ # Only run if OpenAeroStruct is installed try: - from openconcept.aerodynamics.openaerostruct.aerostructural import OASDataGen, Aerostruct, AerostructDragPolar, AerostructDragPolarExact, example_usage + from openconcept.aerodynamics.openaerostruct.aerostructural import ( + OASDataGen, + Aerostruct, + AerostructDragPolar, + AerostructDragPolarExact, + example_usage, + ) OAS_installed = True except ImportError: diff --git a/openconcept/aerodynamics/openaerostruct/tests/test_drag_polar.py b/openconcept/aerodynamics/openaerostruct/tests/test_drag_polar.py index 38237a49..9e255186 100644 --- a/openconcept/aerodynamics/openaerostruct/tests/test_drag_polar.py +++ b/openconcept/aerodynamics/openaerostruct/tests/test_drag_polar.py @@ -7,7 +7,13 @@ try: from openaerostruct.geometry.geometry_group import Geometry from openaerostruct.aerodynamics.aero_groups import AeroPoint - from openconcept.aerodynamics.openaerostruct.drag_polar import VLMDataGen, VLM, VLMDragPolar, PlanformMesh, example_usage + from openconcept.aerodynamics.openaerostruct.drag_polar import ( + VLMDataGen, + VLM, + VLMDragPolar, + PlanformMesh, + example_usage, + ) OAS_installed = True except ImportError: diff --git a/openconcept/examples/HybridTwin.py b/openconcept/examples/HybridTwin.py index f4f0a7ff..6e933bf3 100644 --- a/openconcept/examples/HybridTwin.py +++ b/openconcept/examples/HybridTwin.py @@ -2,7 +2,17 @@ import logging import numpy as np -from openmdao.api import Problem, Group, ScipyOptimizeDriver, ExplicitComponent, ExecComp, SqliteRecorder, DirectSolver, IndepVarComp, NewtonSolver +from openmdao.api import ( + Problem, + Group, + ScipyOptimizeDriver, + ExplicitComponent, + ExecComp, + SqliteRecorder, + DirectSolver, + IndepVarComp, + NewtonSolver, +) # imports for the airplane model itself from openconcept.aerodynamics import PolarDrag diff --git a/openconcept/mission/tests/test_trajectories.py b/openconcept/mission/tests/test_trajectories.py index fac098f8..e12c5163 100644 --- a/openconcept/mission/tests/test_trajectories.py +++ b/openconcept/mission/tests/test_trajectories.py @@ -675,9 +675,7 @@ def initialize(self): def setup(self): nn = self.options["num_nodes"] self.add_subsystem("iv", om.IndepVarComp("duration", val=5.0, units="s"), promotes_outputs=["*"]) - self.add_subsystem( - "a", IntegratorGroupTestBase(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"] - ) + self.add_subsystem("a", IntegratorGroupTestBase(num_nodes=nn), promotes_inputs=["*"], promotes_outputs=["*"]) # promote the outputs of b self.add_subsystem( "b", IntegratorTestMultipleOutputs(num_nodes=nn), promotes_outputs=["*f2*"], promotes_inputs=["*df2"] diff --git a/openconcept/thermal/chiller.py b/openconcept/thermal/chiller.py index ad79ce50..ff46d79e 100644 --- a/openconcept/thermal/chiller.py +++ b/openconcept/thermal/chiller.py @@ -237,9 +237,7 @@ def setup(self): iv.add_output("bypass_start", val=1.0) iv.add_output("bypass_end", val=1.0) - self.add_subsystem( - "li", LinearInterpolator(num_nodes=nn, units=None), promotes_outputs=[("vec", "bypass")] - ) + self.add_subsystem("li", LinearInterpolator(num_nodes=nn, units=None), promotes_outputs=[("vec", "bypass")]) self.connect("control.bypass_start", "li.start_val") self.connect("control.bypass_end", "li.end_val") self.add_subsystem( From 4b9048b647b2a7587e057fbfcf583a30f8d5dfab Mon Sep 17 00:00:00 2001 From: eytanadler Date: Thu, 11 Aug 2022 10:59:21 -0400 Subject: [PATCH 5/7] Moved docs to doc folder --- .gitignore | 4 ++-- .readthedocs.yml | 2 +- {docs => doc}/Makefile | 0 {docs => doc}/__init__.py | 0 {docs => doc}/_exts/__init__.py | 0 {docs => doc}/_exts/docutil_legacy.py | 0 {docs => doc}/_exts/embed_bibtex.py | 0 {docs => doc}/_exts/embed_code.py | 0 {docs => doc}/_exts/embed_compare.py | 0 {docs => doc}/_exts/embed_n2.py | 0 {docs => doc}/_exts/embed_options.py | 0 {docs => doc}/_exts/embed_shell_cmd.py | 0 {docs => doc}/_exts/link_class_from_docstring.py | 2 +- {docs => doc}/_exts/tags.py | 0 .../_static/images/full_parallel_system_chiller.png | Bin {docs => doc}/_static/images/readme_charts.png | Bin {docs => doc}/conf.py | 4 ++-- {docs => doc}/developer/roadmap.rst | 0 {docs => doc}/features/aerodynamics.rst | 0 {docs => doc}/features/atmospherics.rst | 0 {docs => doc}/features/costs.rst | 0 {docs => doc}/features/energy_storage.rst | 0 {docs => doc}/features/mission.rst | 0 {docs => doc}/features/propulsion.rst | 0 {docs => doc}/features/thermal.rst | 0 {docs => doc}/features/utilities.rst | 0 {docs => doc}/features/weights.rst | 0 {docs => doc}/index.rst | 0 {docs => doc}/make.bat | 0 {docs => doc}/publications.rst | 0 {docs => doc}/ref.bib | 0 {docs => doc}/requirements.txt | 0 {docs => doc}/tutorials/integrator.rst | 0 {docs => doc}/tutorials/minimal_example.rst | 0 {docs => doc}/tutorials/more_examples.rst | 0 {docs => doc}/tutorials/readme.md | 0 {docs => doc}/tutorials/turboprop.rst | 0 readme.md | 8 ++++---- 38 files changed, 10 insertions(+), 10 deletions(-) rename {docs => doc}/Makefile (100%) rename {docs => doc}/__init__.py (100%) rename {docs => doc}/_exts/__init__.py (100%) rename {docs => doc}/_exts/docutil_legacy.py (100%) rename {docs => doc}/_exts/embed_bibtex.py (100%) rename {docs => doc}/_exts/embed_code.py (100%) rename {docs => doc}/_exts/embed_compare.py (100%) rename {docs => doc}/_exts/embed_n2.py (100%) rename {docs => doc}/_exts/embed_options.py (100%) rename {docs => doc}/_exts/embed_shell_cmd.py (100%) rename {docs => doc}/_exts/link_class_from_docstring.py (98%) rename {docs => doc}/_exts/tags.py (100%) rename {docs => doc}/_static/images/full_parallel_system_chiller.png (100%) rename {docs => doc}/_static/images/readme_charts.png (100%) rename {docs => doc}/conf.py (99%) rename {docs => doc}/developer/roadmap.rst (100%) rename {docs => doc}/features/aerodynamics.rst (100%) rename {docs => doc}/features/atmospherics.rst (100%) rename {docs => doc}/features/costs.rst (100%) rename {docs => doc}/features/energy_storage.rst (100%) rename {docs => doc}/features/mission.rst (100%) rename {docs => doc}/features/propulsion.rst (100%) rename {docs => doc}/features/thermal.rst (100%) rename {docs => doc}/features/utilities.rst (100%) rename {docs => doc}/features/weights.rst (100%) rename {docs => doc}/index.rst (100%) rename {docs => doc}/make.bat (100%) rename {docs => doc}/publications.rst (100%) rename {docs => doc}/ref.bib (100%) rename {docs => doc}/requirements.txt (100%) rename {docs => doc}/tutorials/integrator.rst (100%) rename {docs => doc}/tutorials/minimal_example.rst (100%) rename {docs => doc}/tutorials/more_examples.rst (100%) rename {docs => doc}/tutorials/readme.md (100%) rename {docs => doc}/tutorials/turboprop.rst (100%) diff --git a/.gitignore b/.gitignore index cdc922b5..5f946bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ .cache/ build/ *pycache* -docs/_build -docs/_srcdocs +doc/_build +doc/_srcdocs *_old.py *hide* coloring.json diff --git a/.readthedocs.yml b/.readthedocs.yml index cf645257..539d5506 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,7 +1,7 @@ version: 2 sphinx: - configuration: docs/conf.py + configuration: doc/conf.py formats: - htmlzip diff --git a/docs/Makefile b/doc/Makefile similarity index 100% rename from docs/Makefile rename to doc/Makefile diff --git a/docs/__init__.py b/doc/__init__.py similarity index 100% rename from docs/__init__.py rename to doc/__init__.py diff --git a/docs/_exts/__init__.py b/doc/_exts/__init__.py similarity index 100% rename from docs/_exts/__init__.py rename to doc/_exts/__init__.py diff --git a/docs/_exts/docutil_legacy.py b/doc/_exts/docutil_legacy.py similarity index 100% rename from docs/_exts/docutil_legacy.py rename to doc/_exts/docutil_legacy.py diff --git a/docs/_exts/embed_bibtex.py b/doc/_exts/embed_bibtex.py similarity index 100% rename from docs/_exts/embed_bibtex.py rename to doc/_exts/embed_bibtex.py diff --git a/docs/_exts/embed_code.py b/doc/_exts/embed_code.py similarity index 100% rename from docs/_exts/embed_code.py rename to doc/_exts/embed_code.py diff --git a/docs/_exts/embed_compare.py b/doc/_exts/embed_compare.py similarity index 100% rename from docs/_exts/embed_compare.py rename to doc/_exts/embed_compare.py diff --git a/docs/_exts/embed_n2.py b/doc/_exts/embed_n2.py similarity index 100% rename from docs/_exts/embed_n2.py rename to doc/_exts/embed_n2.py diff --git a/docs/_exts/embed_options.py b/doc/_exts/embed_options.py similarity index 100% rename from docs/_exts/embed_options.py rename to doc/_exts/embed_options.py diff --git a/docs/_exts/embed_shell_cmd.py b/doc/_exts/embed_shell_cmd.py similarity index 100% rename from docs/_exts/embed_shell_cmd.py rename to doc/_exts/embed_shell_cmd.py diff --git a/docs/_exts/link_class_from_docstring.py b/doc/_exts/link_class_from_docstring.py similarity index 98% rename from docs/_exts/link_class_from_docstring.py rename to doc/_exts/link_class_from_docstring.py index 8b1c2684..39f03f3d 100644 --- a/docs/_exts/link_class_from_docstring.py +++ b/doc/_exts/link_class_from_docstring.py @@ -19,7 +19,7 @@ def build_dict(): path=package.__path__, prefix=package.__name__ + ".", onerror=lambda x: None ): if not ispkg: - if "docs" not in modname: + if "doc" not in modname: if any(ignore in modname for ignore in IGNORE_LIST): continue module = importer.find_module(modname).load_module(modname) diff --git a/docs/_exts/tags.py b/doc/_exts/tags.py similarity index 100% rename from docs/_exts/tags.py rename to doc/_exts/tags.py diff --git a/docs/_static/images/full_parallel_system_chiller.png b/doc/_static/images/full_parallel_system_chiller.png similarity index 100% rename from docs/_static/images/full_parallel_system_chiller.png rename to doc/_static/images/full_parallel_system_chiller.png diff --git a/docs/_static/images/readme_charts.png b/doc/_static/images/readme_charts.png similarity index 100% rename from docs/_static/images/readme_charts.png rename to doc/_static/images/readme_charts.png diff --git a/docs/conf.py b/doc/conf.py similarity index 99% rename from docs/conf.py rename to doc/conf.py index c140de31..896be85f 100644 --- a/docs/conf.py +++ b/doc/conf.py @@ -26,7 +26,7 @@ # so we add it to the path this_directory = os.path.abspath(os.path.dirname(__file__)) -# sys.path.insert(0, os.path.abspath(openconcept.__path__[0]+r'/docs/_exts')) +# sys.path.insert(0, os.path.abspath(openconcept.__path__[0]+r'/doc/_exts')) def generate_src_docs(dir, top, packages): @@ -167,7 +167,7 @@ def run_file_move_result(file_name, output_files, destination_files, optional_cl This function can be used to automatically generate the figure in the RTD build and move it to a specific location in the RTD build. - Note that the file is run from the openconcept/docs directory and all relative paths + Note that the file is run from the openconcept/doc directory and all relative paths are relative to this directory. If the output file name is defined in the script using a relative path remember to take it into account. diff --git a/docs/developer/roadmap.rst b/doc/developer/roadmap.rst similarity index 100% rename from docs/developer/roadmap.rst rename to doc/developer/roadmap.rst diff --git a/docs/features/aerodynamics.rst b/doc/features/aerodynamics.rst similarity index 100% rename from docs/features/aerodynamics.rst rename to doc/features/aerodynamics.rst diff --git a/docs/features/atmospherics.rst b/doc/features/atmospherics.rst similarity index 100% rename from docs/features/atmospherics.rst rename to doc/features/atmospherics.rst diff --git a/docs/features/costs.rst b/doc/features/costs.rst similarity index 100% rename from docs/features/costs.rst rename to doc/features/costs.rst diff --git a/docs/features/energy_storage.rst b/doc/features/energy_storage.rst similarity index 100% rename from docs/features/energy_storage.rst rename to doc/features/energy_storage.rst diff --git a/docs/features/mission.rst b/doc/features/mission.rst similarity index 100% rename from docs/features/mission.rst rename to doc/features/mission.rst diff --git a/docs/features/propulsion.rst b/doc/features/propulsion.rst similarity index 100% rename from docs/features/propulsion.rst rename to doc/features/propulsion.rst diff --git a/docs/features/thermal.rst b/doc/features/thermal.rst similarity index 100% rename from docs/features/thermal.rst rename to doc/features/thermal.rst diff --git a/docs/features/utilities.rst b/doc/features/utilities.rst similarity index 100% rename from docs/features/utilities.rst rename to doc/features/utilities.rst diff --git a/docs/features/weights.rst b/doc/features/weights.rst similarity index 100% rename from docs/features/weights.rst rename to doc/features/weights.rst diff --git a/docs/index.rst b/doc/index.rst similarity index 100% rename from docs/index.rst rename to doc/index.rst diff --git a/docs/make.bat b/doc/make.bat similarity index 100% rename from docs/make.bat rename to doc/make.bat diff --git a/docs/publications.rst b/doc/publications.rst similarity index 100% rename from docs/publications.rst rename to doc/publications.rst diff --git a/docs/ref.bib b/doc/ref.bib similarity index 100% rename from docs/ref.bib rename to doc/ref.bib diff --git a/docs/requirements.txt b/doc/requirements.txt similarity index 100% rename from docs/requirements.txt rename to doc/requirements.txt diff --git a/docs/tutorials/integrator.rst b/doc/tutorials/integrator.rst similarity index 100% rename from docs/tutorials/integrator.rst rename to doc/tutorials/integrator.rst diff --git a/docs/tutorials/minimal_example.rst b/doc/tutorials/minimal_example.rst similarity index 100% rename from docs/tutorials/minimal_example.rst rename to doc/tutorials/minimal_example.rst diff --git a/docs/tutorials/more_examples.rst b/doc/tutorials/more_examples.rst similarity index 100% rename from docs/tutorials/more_examples.rst rename to doc/tutorials/more_examples.rst diff --git a/docs/tutorials/readme.md b/doc/tutorials/readme.md similarity index 100% rename from docs/tutorials/readme.md rename to doc/tutorials/readme.md diff --git a/docs/tutorials/turboprop.rst b/doc/tutorials/turboprop.rst similarity index 100% rename from docs/tutorials/turboprop.rst rename to doc/tutorials/turboprop.rst diff --git a/readme.md b/readme.md index 12d2f0b9..58fcd694 100644 --- a/readme.md +++ b/readme.md @@ -14,12 +14,12 @@ OpenConcept is capable of modeling a wide range of propulsion systems, including The following figure (from [this paper](https://doi.org/10.3390/aerospace9050243)) shows one such system that is modeled in the `N3_HybridSingleAisle_Refrig.py` example.

- +

The following charts show more than 250 individually optimized hybrid-electric light twin aircraft (similar to a King Air C90GT). Optimizing hundreds of configurations can be done in a couple of hours on a standard laptop computer. -![Example charts](/docs/_static/images/readme_charts.png) +![Example charts](/doc/_static/images/readme_charts.png) The reason for OpenConcept's efficiency is the analytic derivatives built into each analysis routine and component. Accurate, efficient derivatives enable the use of Newton nonlinear equation solutions and gradient-based optimization at low computational cost. @@ -27,7 +27,7 @@ The reason for OpenConcept's efficiency is the analytic derivatives built into e Automatically-generated documentation is available at (https://mdolab-openconcept.readthedocs-hosted.com/en/latest/). -To build the docs locally, install the `sphinx_mdolab_theme` via `pip`. Then enter the `docs` folder in the root directory and run `make html`. The built documentation can be viewed by opening `_build/html/index.html`. OpenAeroStruct is required (also installable via `pip`) to build the OpenAeroStruct portion of the source docs. +To build the docs locally, install the `sphinx_mdolab_theme` via `pip`. Then enter the `doc` folder in the root directory and run `make html`. The built documentation can be viewed by opening `_build/html/index.html`. OpenAeroStruct is required (also installable via `pip`) to build the OpenAeroStruct portion of the source docs. ## Getting Started @@ -47,7 +47,7 @@ The features section of the documentation describes most of the components and s ### Requirements - + This toolkit requires the use of [OpenMDAO](https://openmdao.org/) 3.10.0 or later. OpenMDAO requires a late NumPy and SciPy. From 580ea8e130f9e44bcef80ab341b66b81e43213cb Mon Sep 17 00:00:00 2001 From: eytanadler Date: Thu, 11 Aug 2022 11:07:30 -0400 Subject: [PATCH 6/7] Removed docs _ext and a few more formatting changes --- doc/_exts/__init__.py | 0 doc/_exts/docutil_legacy.py | 925 ------------------ doc/_exts/embed_bibtex.py | 77 -- doc/_exts/embed_code.py | 324 ------ doc/_exts/embed_compare.py | 146 --- doc/_exts/embed_n2.py | 106 -- doc/_exts/embed_options.py | 69 -- doc/_exts/embed_shell_cmd.py | 166 ---- doc/_exts/link_class_from_docstring.py | 87 -- doc/_exts/tags.py | 80 -- doc/conf.py | 3 - openconcept/examples/B738_aerostructural.py | 6 +- openconcept/examples/minimal.py | 4 +- openconcept/examples/minimal_integrator.py | 4 +- .../utilities/math/multiply_divide_comp.py | 2 +- 15 files changed, 8 insertions(+), 1991 deletions(-) delete mode 100644 doc/_exts/__init__.py delete mode 100644 doc/_exts/docutil_legacy.py delete mode 100644 doc/_exts/embed_bibtex.py delete mode 100644 doc/_exts/embed_code.py delete mode 100644 doc/_exts/embed_compare.py delete mode 100644 doc/_exts/embed_n2.py delete mode 100644 doc/_exts/embed_options.py delete mode 100644 doc/_exts/embed_shell_cmd.py delete mode 100644 doc/_exts/link_class_from_docstring.py delete mode 100644 doc/_exts/tags.py diff --git a/doc/_exts/__init__.py b/doc/_exts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/doc/_exts/docutil_legacy.py b/doc/_exts/docutil_legacy.py deleted file mode 100644 index b7897f0d..00000000 --- a/doc/_exts/docutil_legacy.py +++ /dev/null @@ -1,925 +0,0 @@ -""" -A collection of functions for modifying source code that is embeded into the Sphinx documentation. -""" - -import sys -import os -import re -import tokenize -import importlib -import inspect -import subprocess -import tempfile -import unittest -import traceback -import ast - -from docutils import nodes - -from collections import namedtuple - -from io import StringIO - -from sphinx.errors import SphinxError -from sphinx.writers.html import HTMLTranslator -from sphinx.writers.html5 import HTML5Translator -from redbaron import RedBaron - -import html as cgiesc - -from openmdao.utils.general_utils import printoptions - -sqlite_file = "feature_docs_unit_test_db.sqlite" # name of the sqlite database file -table_name = "feature_unit_tests" # name of the table to be queried - -_sub_runner = os.path.join(os.path.dirname(os.path.abspath(__file__)), "run_sub.py") - - -# an input block consists of a block of code and a tag that marks the end of any -# output from that code in the output stream (via inserted print('>>>>>#') statements) -InputBlock = namedtuple("InputBlock", "code tag") - - -class skipped_or_failed_node(nodes.Element): - pass - - -def visit_skipped_or_failed_node(self, node): - pass - - -def depart_skipped_or_failed_node(self, node): - if not isinstance(self, (HTMLTranslator, HTML5Translator)): - self.body.append("output only available for HTML\n") - return - - html = '
{}
'.format( - node["kind"], node["text"] - ) - self.body.append(html) - - -class in_or_out_node(nodes.Element): - pass - - -def visit_in_or_out_node(self, node): - pass - - -def depart_in_or_out_node(self, node): - """ - This function creates the formatting that sets up the look of the blocks. - The look of the formatting is controlled by _theme/static/style.css - """ - if not isinstance(self, (HTMLTranslator, HTML5Translator)): - self.body.append("output only available for HTML\n") - return - if node["kind"] == "In": - html = '
{}
'.format(node["text"]) - elif node["kind"] == "Out": - html = '
{}
'.format( - node["text"] - ) - - self.body.append(html) - - -def node_setup(app): - app.add_node(skipped_or_failed_node, html=(visit_skipped_or_failed_node, depart_skipped_or_failed_node)) - app.add_node(in_or_out_node, html=(visit_in_or_out_node, depart_in_or_out_node)) - - -def remove_docstrings(source): - """ - Return 'source' minus docstrings. - - Parameters - ---------- - source : str - Original source code. - - Returns - ------- - str - Source with docstrings removed. - """ - io_obj = StringIO(source) - out = "" - prev_toktype = tokenize.INDENT - last_lineno = -1 - last_col = 0 - for tok in tokenize.generate_tokens(io_obj.readline): - token_type = tok[0] - token_string = tok[1] - start_line, start_col = tok[2] - end_line, end_col = tok[3] - # ltext = tok[4] # in original code but not used here - # The following two conditionals preserve indentation. - # This is necessary because we're not using tokenize.untokenize() - # (because it spits out code with copious amounts of oddly-placed - # whitespace). - if start_line > last_lineno: - last_col = 0 - if start_col > last_col: - out += " " * (start_col - last_col) - # This series of conditionals removes docstrings: - if token_type == tokenize.STRING: - if prev_toktype != tokenize.INDENT: - # This is likely a docstring; double-check we're not inside an operator: - if prev_toktype != tokenize.NEWLINE: - # Note regarding NEWLINE vs NL: The tokenize module - # differentiates between newlines that start a new statement - # and newlines inside of operators such as parens, brackes, - # and curly braces. Newlines inside of operators are - # NEWLINE and newlines that start new code are NL. - # Catch whole-module docstrings: - if start_col > 0: - # Unlabelled indentation means we're inside an operator - out += token_string - # Note regarding the INDENT token: The tokenize module does - # not label indentation inside of an operator (parens, - # brackets, and curly braces) as actual indentation. - # For example: - # def foo(): - # "The spaces before this docstring are tokenize.INDENT" - # test = [ - # "The spaces before this string do not get a token" - # ] - else: - out += token_string - prev_toktype = token_type - last_col = end_col - last_lineno = end_line - return out - - -def remove_redbaron_node(node, index): - """ - Utility function for removing a node using RedBaron. - - RedBaron has some problems with modifying code lines that run across - multiple lines. ( It is mentioned somewhere online but cannot seem to - find it now. ) - - RedBaron throws an Exception but when you check, it seems like it does - what you asked it to do. So, for now, we ignore the Exception. - """ - - try: - node.value.remove(node.value[index]) - except Exception as e: # no choice but to catch the general Exception - if str(e).startswith("It appears that you have indentation in your CommaList"): - pass - else: - raise - - -def replace_asserts_with_prints(src): - """ - Replace asserts with print statements. - - Using RedBaron, replace some assert calls with print statements that print the actual - value given in the asserts. Depending on the calls, the actual value can be the first or second - argument. - - Parameters - ---------- - src : str - String containing source lines. - - Returns - ------- - str - String containing source with asserts replaced by prints. - """ - rb = RedBaron(src) # convert to RedBaron internal structure - - # findAll is slow, so only check the ones that are present. - base_assert = [ - "assertAlmostEqual", - "assertLess", - "assertGreater", - "assertEqual", - "assert_equal_arrays", - "assertTrue", - "assertFalse", - ] - used_assert = [item for item in base_assert if item in src] - - for assert_type in used_assert: - assert_nodes = rb.findAll("NameNode", value=assert_type) - for assert_node in assert_nodes: - assert_node = assert_node.parent - remove_redbaron_node(assert_node, 0) # remove 'self' from the call - assert_node.value[0].replace("print") - if assert_type not in ["assertTrue", "assertFalse"]: - # remove the expected value argument - remove_redbaron_node(assert_node.value[1], 1) - - if "assert_rel_error" in src: - assert_nodes = rb.findAll("NameNode", value="assert_rel_error") - for assert_node in assert_nodes: - assert_node = assert_node.parent - # If relative error tolerance is specified, there are 4 arguments - if len(assert_node.value[1]) == 4: - # remove the relative error tolerance - remove_redbaron_node(assert_node.value[1], -1) - remove_redbaron_node(assert_node.value[1], -1) # remove the expected value - # remove the first argument which is the TestCase - remove_redbaron_node(assert_node.value[1], 0) - # - assert_node.value[0].replace("print") - - if "assert_near_equal" in src: - assert_nodes = rb.findAll("NameNode", value="assert_near_equal") - for assert_node in assert_nodes: - assert_node = assert_node.parent - # If relative error tolerance is specified, there are 3 arguments - if len(assert_node.value[1]) == 3: - # remove the relative error tolerance - remove_redbaron_node(assert_node.value[1], -1) - remove_redbaron_node(assert_node.value[1], -1) # remove the expected value - assert_node.value[0].replace("print") - - if "assert_almost_equal" in src: - assert_nodes = rb.findAll("NameNode", value="assert_almost_equal") - for assert_node in assert_nodes: - assert_node = assert_node.parent - # If relative error tolerance is specified, there are 3 arguments - if len(assert_node.value[1]) == 3: - # remove the relative error tolerance - remove_redbaron_node(assert_node.value[1], -1) - remove_redbaron_node(assert_node.value[1], -1) # remove the expected value - assert_node.value[0].replace("print") - - return rb.dumps() - - -def remove_initial_empty_lines(source): - """ - Some initial empty lines were added to keep RedBaron happy. - Need to strip these out before we pass the source code to the - directive for including source code into feature doc files. - """ - - idx = re.search(r"\S", source, re.MULTILINE).start() - return source[idx:] - - -def get_source_code(path): - """ - Return source code as a text string. - - Parameters - ---------- - path : str - Path to a file, module, function, class, or class method. - - Returns - ------- - str - The source code. - int - Indentation level. - module or None - The imported module. - class or None - The class specified by path. - method or None - The class method specified by path. - """ - - indent = 0 - class_obj = None - method_obj = None - - if path.endswith(".py"): - if not os.path.isfile(path): - raise SphinxError("Can't find file '%s' cwd='%s'" % (path, os.getcwd())) - with open(path, "r") as f: - source = f.read() - module = None - else: - # First, assume module path since we want to support loading a full module as well. - try: - module = importlib.import_module(path) - source = inspect.getsource(module) - - except ImportError: - - # Second, assume class and see if it works - try: - parts = path.split(".") - - module_path = ".".join(parts[:-1]) - module = importlib.import_module(module_path) - class_name = parts[-1] - class_obj = getattr(module, class_name) - source = inspect.getsource(class_obj) - indent = 1 - - except ImportError: - - # else assume it is a path to a method - module_path = ".".join(parts[:-2]) - module = importlib.import_module(module_path) - class_name = parts[-2] - method_name = parts[-1] - class_obj = getattr(module, class_name) - method_obj = getattr(class_obj, method_name) - source = inspect.getsource(method_obj) - indent = 2 - - return remove_leading_trailing_whitespace_lines(source), indent, module, class_obj, method_obj - - -def remove_raise_skip_tests(src): - """ - Remove from the code any raise unittest.SkipTest lines since we don't want those in - what the user sees. - """ - rb = RedBaron(src) - raise_nodes = rb.findAll("RaiseNode") - for rn in raise_nodes: - # only the raise for SkipTest - if rn.value[:2].dumps() == "unittestSkipTest": - rn.parent.value.remove(rn) - return rb.dumps() - - -def remove_leading_trailing_whitespace_lines(src): - """ - Remove any leading or trailing whitespace lines. - - Parameters - ---------- - src : str - Input code. - - Returns - ------- - str - Code with trailing whitespace lines removed. - """ - lines = src.splitlines() - - non_whitespace_lines = [] - for i, l in enumerate(lines): - if l and not l.isspace(): - non_whitespace_lines.append(i) - imin = min(non_whitespace_lines) - imax = max(non_whitespace_lines) - - return "\n".join(lines[imin : imax + 1]) - - -def is_output_node(node): - """ - Determine whether a RedBaron node may be expected to generate output. - - Parameters - ---------- - node : - a RedBaron Node. - - Returns - ------- - bool - True if node may be expected to generate output, otherwise False. - """ - if node.type == "print": - return True - - # lines with the following signatures and function names may generate output - output_signatures = [("name", "name", "call"), ("name", "name", "name", "call")] - output_functions = [ - "setup", - "run_model", - "run_driver", - "check_partials", - "check_totals", - "list_inputs", - "list_outputs", - "list_problem_vars", - ] - - if node.type == "atomtrailers" and len(node.value) in (3, 4): - sig = [] - for val in node.value: - sig.append(val.type) - func_name = node.value[-2].value - if tuple(sig) in output_signatures and func_name in output_functions: - return True - - return False - - -def split_source_into_input_blocks(src): - """ - Split source into blocks; the splits occur at inserted prints. - - Parameters - ---------- - src : str - Input code. - - Returns - ------- - list - List of input code sections. - """ - input_blocks = [] - current_block = [] - - for line in src.splitlines(): - if 'print(">>>>>' in line: - tag = line.split('"')[1] - code = "\n".join(current_block) - input_blocks.append(InputBlock(code, tag)) - current_block = [] - else: - current_block.append(line) - - if len(current_block) > 0: - # final input block, with no associated output - code = "\n".join(current_block) - input_blocks.append(InputBlock(code, "")) - - return input_blocks - - -def insert_output_start_stop_indicators(src): - """ - Insert identifier strings so that output can be segregated from input. - - Parameters - ---------- - src : str - String containing input and output lines. - - Returns - ------- - str - String with output demarked. - """ - lines = src.split("\n") - print_producing = [ - "print(", - ".setup(", - ".run_model(", - ".run_driver(", - ".check_partials(", - ".check_totals(", - ".list_inputs(", - ".list_outputs(", - ".list_sources(", - ".list_source_vars(", - ".list_problem_vars(", - ".list_cases(", - ".list_model_options(", - ".list_solver_options(", - ] - - newlines = [] - input_block_number = 0 - in_try = False - in_continuation = False - head_indent = "" - for line in lines: - newlines.append(line) - - # Check if we are concluding a continuation line. - if in_continuation: - line = line.rstrip() - if not (line.endswith(",") or line.endswith("\\") or line.endswith("(")): - newlines.append('%sprint(">>>>>%d")' % (head_indent, input_block_number)) - input_block_number += 1 - in_continuation = False - - # Don't print if we are in a try block. - if in_try: - if "except" in line: - in_try = False - continue - - if "try:" in line: - in_try = True - continue - - # Searching for 'print(' is a little ambiguous. - if "set_solver_print(" in line: - continue - - for item in print_producing: - if item in line: - indent = " " * (len(line) - len(line.lstrip())) - - # Line continuations are a litle tricky. - line = line.rstrip() - if line.endswith(",") or line.endswith("\\") or line.endswith("("): - in_continuation = True - head_indent = indent - break - - newlines.append('%sprint(">>>>>%d")' % (indent, input_block_number)) - input_block_number += 1 - break - - return "\n".join(newlines) - - -def consolidate_input_blocks(input_blocks, output_blocks): - """ - Merge any input blocks for which there is no corresponding output - with subsequent blocks that do have output. - - Remove any leading and trailing blank lines from all input blocks. - """ - new_input_blocks = [] - new_block = "" - - for (code, tag) in input_blocks: - if tag not in output_blocks: - # no output, add to new consolidated block - if new_block and not new_block.endswith("\n"): - new_block += "\n" - new_block += code - elif new_block: - # add current input to new consolidated block and save - if new_block and not new_block.endswith("\n"): - new_block += "\n" - new_block += code - new_block = remove_leading_trailing_whitespace_lines(new_block) - new_input_blocks.append(InputBlock(new_block, tag)) - new_block = "" - else: - # just strip leading/trailing from input block - code = remove_leading_trailing_whitespace_lines(code) - new_input_blocks.append(InputBlock(code, tag)) - - # trailing input with no corresponding output - if new_block: - new_block = remove_leading_trailing_whitespace_lines(new_block) - new_input_blocks.append(InputBlock(new_block, "")) - - return new_input_blocks - - -def extract_output_blocks(run_output): - """ - Identify and extract outputs from source. - - Parameters - ---------- - run_output : str or list of str - Source code with outputs. - - Returns - ------- - dict - output blocks keyed on tags like ">>>>>4" - """ - if isinstance(run_output, list): - return sync_multi_output_blocks(run_output) - - output_blocks = {} - output_block = None - - for line in run_output.splitlines(): - if output_block is None: - output_block = [] - if line[:5] == ">>>>>": - output = ("\n".join(output_block)).strip() - if output: - output_blocks[line] = output - output_block = None - else: - output_block.append(line) - - if output_block is not None: - # It is possible to have trailing output - # (e.g. if the last print_producing statement is in a try block) - output_blocks["Trailing"] = output_block - - return output_blocks - - -def strip_decorators(src): - """ - Remove any decorators from the source code of the method or function. - - Parameters - ---------- - src : str - Source code - - Returns - ------- - str - Source code minus any decorators - """ - - class Parser(ast.NodeVisitor): - def __init__(self): - self.function_node = None - - def visit_FunctionDef(self, node): - self.function_node = node - - def get_function(self): - return self.function_node - - tree = ast.parse(src) - parser = Parser() - parser.visit(tree) - - # get node for the first function - function_node = parser.get_function() - if not function_node.decorator_list: # no decorators so no changes needed - return src - - # Unfortunately, the ast library, for a decorated function, returns the line - # number for the first decorator when asking for the line number of the function - # So using the line number for the argument for of the function, which is always - # correct. But we assume that the argument is on the same line as the function. - # We also assume there IS an argument. If not, we raise an error. - if function_node.args.args: - function_lineno = function_node.args.args[0].lineno - else: - raise RuntimeError("Cannot determine line number for decorated function without args") - lines = src.splitlines() - - undecorated_src = "\n".join(lines[function_lineno - 1 :]) - - return undecorated_src - - -def strip_header(src): - """ - Directly manipulating function text to strip header, usually or maybe always just the - "def" lines for a method or function. - - This function assumes that the docstring and header, if any, have already been removed. - - Parameters - ---------- - src : str - source code - """ - lines = src.split("\n") - first_len = None - for i, line in enumerate(lines): - n1 = len(line) - newline = line.lstrip() - tab = n1 - len(newline) - if first_len is None: - first_len = tab - elif n1 == 0: - continue - if tab != first_len: - return "\n".join(lines[i:]) - - return "" - - -def dedent(src): - """ - Directly manipulating function text to remove leading whitespace. - - Parameters - ---------- - src : str - source code - """ - - lines = src.split("\n") - if lines: - for i, line in enumerate(lines): - lstrip = line.lstrip() - if lstrip: # keep going if first line(s) are blank. - tab = len(line) - len(lstrip) - return "\n".join(l[tab:] for l in lines[i:]) - return "" - - -def sync_multi_output_blocks(run_output): - """ - Combine output from different procs into the same output blocks. - - Parameters - ---------- - run_output : list of dict - List of outputs from individual procs. - - Returns - ------- - dict - Synced output blocks from all procs. - """ - if run_output: - # for each proc's run output, get a dict of output blocks keyed by tag - proc_output_blocks = [extract_output_blocks(outp) for outp in run_output] - - synced_blocks = {} - - for i, outp in enumerate(proc_output_blocks): - for tag in outp: - if str(outp[tag]).strip(): - if tag in synced_blocks: - synced_blocks[tag] += "(rank %d) %s\n" % (i, outp[tag]) - else: - synced_blocks[tag] = "(rank %d) %s\n" % (i, outp[tag]) - - return synced_blocks - else: - return {} - - -def run_code(code_to_run, path, module=None, cls=None, shows_plot=False, imports_not_required=False): - """ - Run the given code chunk and collect the output. - """ - - skipped = False - failed = False - - if cls is None: - use_mpi = False - else: - try: - import mpi4py - except ImportError: - use_mpi = False - else: - N_PROCS = getattr(cls, "N_PROCS", 1) - use_mpi = N_PROCS > 1 - - try: - # use subprocess to run code to avoid any nasty interactions between codes - - # Move to the test directory in case there are files to read. - save_dir = os.getcwd() - - if module is None: - code_dir = os.path.dirname(os.path.abspath(path)) - else: - code_dir = os.path.dirname(os.path.abspath(module.__file__)) - - os.chdir(code_dir) - - if use_mpi: - env = os.environ.copy() - - # output will be written to one file per process - env["USE_PROC_FILES"] = "1" - - env["OPENMDAO_CURRENT_MODULE"] = module.__name__ - env["OPENMDAO_CODE_TO_RUN"] = code_to_run - - p = subprocess.Popen(["mpirun", "-n", str(N_PROCS), sys.executable, _sub_runner], env=env) - p.wait() - - # extract output blocks from all output files & merge them - output = [] - for i in range(N_PROCS): - with open("%d.out" % i) as f: - output.append(f.read()) - os.remove("%d.out" % i) - - elif shows_plot: - if module is None: - # write code to a file so we can run it. - fd, code_to_run_path = tempfile.mkstemp() - with os.fdopen(fd, "w") as tmp: - tmp.write(code_to_run) - try: - p = subprocess.Popen( - [sys.executable, code_to_run_path], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=os.environ, - ) - output, _ = p.communicate() - if p.returncode != 0: - failed = True - - finally: - os.remove(code_to_run_path) - else: - env = os.environ.copy() - - env["OPENMDAO_CURRENT_MODULE"] = module.__name__ - env["OPENMDAO_CODE_TO_RUN"] = code_to_run - - p = subprocess.Popen( - [sys.executable, _sub_runner], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env - ) - output, _ = p.communicate() - if p.returncode != 0: - failed = True - - output = output.decode("utf-8", "ignore") - else: - # just exec() the code for serial tests. - - # capture all output - stdout = sys.stdout - stderr = sys.stderr - strout = StringIO() - sys.stdout = strout - sys.stderr = strout - - # We need more precision from numpy - with printoptions(precision=8): - - if module is None: - globals_dict = { - "__file__": path, - "__name__": "__main__", - "__package__": None, - "__cached__": None, - } - else: - if imports_not_required: - # code does not need to include all imports - # Get from module - globals_dict = module.__dict__ - else: - globals_dict = {} - - try: - exec(code_to_run, globals_dict) - except Exception as err: - # for actual errors, print code (with line numbers) to facilitate debugging - if not isinstance(err, unittest.SkipTest): - for n, line in enumerate(code_to_run.split("\n")): - print("%4d: %s" % (n, line), file=stderr) - raise - finally: - sys.stdout = stdout - sys.stderr = stderr - - output = strout.getvalue() - - except subprocess.CalledProcessError as e: - output = e.output.decode("utf-8", "ignore") - # Get a traceback. - if "raise unittest.SkipTest" in output: - reason_for_skip = output.splitlines()[-1][len("unittest.case.SkipTest: ") :] - output = reason_for_skip - skipped = True - else: - output = "Running of embedded code {} in docs failed due to: \n\n{}".format(path, output) - failed = True - except unittest.SkipTest as skip: - output = str(skip) - skipped = True - except Exception as exc: - output = "Running of embedded code {} in docs failed due to: \n\n{}".format(path, traceback.format_exc()) - failed = True - finally: - os.chdir(save_dir) - - return skipped, failed, output - - -def get_skip_output_node(output): - output = "Test skipped because " + output - return skipped_or_failed_node(text=output, number=1, kind="skipped") - - -def get_interleaved_io_nodes(input_blocks, output_blocks): - """ - Parameters - ---------- - input_blocks : list of tuple - Each tuple is a block of code and the tag marking it's output. - - output_blocks : dict - Output blocks keyed on tag. - """ - nodelist = [] - n = 1 - - for (code, tag) in input_blocks: - input_node = nodes.literal_block(code, code) - input_node["language"] = "python" - nodelist.append(input_node) - if tag and tag in output_blocks: - outp = cgiesc.escape(output_blocks[tag]) - if outp.strip(): - output_node = in_or_out_node(kind="Out", number=n, text=outp) - nodelist.append(output_node) - n += 1 - - if "Trailing" in output_blocks: - output_node = in_or_out_node(kind="Out", number=n, text=output_blocks["Trailing"]) - nodelist.append(output_node) - - return nodelist - - -def get_output_block_node(output_blocks): - output_block = "\n".join([cgiesc.escape(ob) for ob in output_blocks]) - return in_or_out_node(kind="Out", number=1, text=output_block) diff --git a/doc/_exts/embed_bibtex.py b/doc/_exts/embed_bibtex.py deleted file mode 100644 index fe3e072a..00000000 --- a/doc/_exts/embed_bibtex.py +++ /dev/null @@ -1,77 +0,0 @@ -import sys -import importlib - -from docutils import nodes - -import sphinx -from docutils.parsers.rst import Directive - -from sphinx.writers.html import HTMLTranslator -from sphinx.writers.html5 import HTML5Translator -from sphinx.errors import SphinxError - - -class bibtex_node(nodes.Element): - pass - - -def visit_bibtex_node(self, node): - pass - - -def depart_bibtex_node(self, node): - """ - This function creates the formatting that sets up the look of the blocks. - The look of the formatting is controlled by _theme/static/style.css - """ - if not isinstance(self, (HTMLTranslator, HTML5Translator)): - self.body.append("output only available for HTML\n") - return - - html = """ -
-
{}
-
""".format( - node["text"] - ) - - self.body.append(html) - - -class EmbedBibtexDirective(Directive): - """ - EmbedBibtexDirective is a custom directive to allow a Bibtex citation to be embedded. - - .. embed-bibtex:: - openmdao.solvers.linear.petsc_ksp - PETScKrylov - - - The 2 arguments are the module path and the class name. - - What the above will do is replace the directive and its args with the Bibtex citation - for the class. - - """ - - required_arguments = 2 - optional_arguments = 0 - has_content = True - - def run(self): - module_path, class_name = self.arguments - mod = importlib.import_module(module_path) - obj = getattr(mod, class_name)() - - if not hasattr(obj, "cite") or not obj.cite: - raise SphinxError("Couldn't find 'cite' in class '%s'" % class_name) - - return [bibtex_node(text=obj.cite)] - - -def setup(app): - """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive("embed-bibtex", EmbedBibtexDirective) - app.add_node(bibtex_node, html=(visit_bibtex_node, depart_bibtex_node)) - - return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/doc/_exts/embed_code.py b/doc/_exts/embed_code.py deleted file mode 100644 index 80c86c1d..00000000 --- a/doc/_exts/embed_code.py +++ /dev/null @@ -1,324 +0,0 @@ -import unittest -from docutils import nodes -from docutils.parsers.rst import Directive -import re -from sphinx.errors import SphinxError -import sphinx -import traceback -import inspect -import os -import sys - -from docutils.parsers.rst.directives import unchanged, images - -this_directory = os.path.abspath(os.path.dirname(__file__)) -sys.path.insert(0, this_directory) - - -from docutil_legacy import ( - get_source_code, - remove_docstrings, - remove_initial_empty_lines, - replace_asserts_with_prints, - strip_header, - dedent, - insert_output_start_stop_indicators, - run_code, - get_skip_output_node, - get_interleaved_io_nodes, - get_output_block_node, - split_source_into_input_blocks, - extract_output_blocks, - consolidate_input_blocks, - node_setup, - strip_decorators, -) - - -_plot_count = 0 - -plotting_functions = ["\.show\(", "partial_deriv_plot\("] - - -class EmbedCodeDirective(Directive): - """ - EmbedCodeDirective is a custom directive to allow blocks of - python code to be shown in feature docs. An example usage would look like this: - - .. embed-code:: - openmdao.test.whatever.method - - What the above will do is replace the directive and its args with the block of code - for the class or method desired. - - By default, docstrings will be kept in the embedded code. There is an option - to the directive to strip the docstrings: - - .. embed-code:: - openmdao.test.whatever.method - :strip-docstrings: - """ - - # must have at least one directive for this to work - required_arguments = 1 - has_content = True - - option_spec = { - "strip-docstrings": unchanged, - "layout": unchanged, - "scale": unchanged, - "align": unchanged, - "imports-not-required": unchanged, - } - - def run(self): - global _plot_count - - # - # error checking - # - allowed_layouts = set(["code", "output", "interleave", "plot"]) - - if "layout" in self.options: - layout = [s.strip() for s in self.options["layout"].split(",")] - else: - layout = ["code"] - - if len(layout) > len(set(layout)): - raise SphinxError("No duplicate layout entries allowed.") - - bad = [n for n in layout if n not in allowed_layouts] - if bad: - raise SphinxError("The following layout options are invalid: %s" % bad) - - if "interleave" in layout and ("code" in layout or "output" in layout): - raise SphinxError("The interleave option is mutually exclusive to the code " "and output options.") - - # - # Get the source code - # - path = self.arguments[0] - try: - source, indent, module, class_, method = get_source_code(path) - except Exception as err: - # Generally means the source couldn't be inspected or imported. - # Raise as a Directive warning (level 2 in docutils). - # This way, the sphinx build does not terminate if, for example, you are building on - # an environment where mpi or pyoptsparse are missing. - raise self.directive_error(2, str(err)) - - # - # script, test and/or plot? - # - is_script = path.endswith(".py") - - is_test = class_ is not None and inspect.isclass(class_) and issubclass(class_, unittest.TestCase) - - shows_plot = re.compile("|".join(plotting_functions)).search(source) - - if "plot" in layout: - plot_dir = os.getcwd() - plot_fname = "doc_plot_%d.png" % _plot_count - _plot_count += 1 - - plot_file_abs = os.path.join(os.path.abspath(plot_dir), plot_fname) - if os.path.isfile(plot_file_abs): - # remove any existing plot file - os.remove(plot_file_abs) - - # - # Modify the source prior to running - # - if "strip-docstrings" in self.options: - source = remove_docstrings(source) - - setup_code = "" - teardown_code = "" - mpl_import = "" - mpl_figure = "" - - if is_test: - try: - source = dedent(source) - source = strip_decorators(source) - source = strip_header(source) - source = dedent(source) - source = replace_asserts_with_prints(source) - source = remove_initial_empty_lines(source) - - class_name = class_.__name__ - method_name = path.rsplit(".", 1)[1] - - # make 'self' available to test code (as an instance of the test case) - self_code = "from %s import %s\nself = %s('%s')\n" % ( - module.__name__, - class_name, - class_name, - method_name, - ) - - # get setUp and tearDown but don't duplicate if it is the method being tested - setup_code = ( - "" - if method_name == "setUp" - else dedent(strip_header(remove_docstrings(inspect.getsource(getattr(class_, "setUp"))))) - ) - - teardown_code = ( - "" - if method_name == "tearDown" - else dedent(strip_header(remove_docstrings(inspect.getsource(getattr(class_, "tearDown"))))) - ) - - # for interleaving, we need to mark input/output blocks - if "interleave" in layout: - interleaved = insert_output_start_stop_indicators(source) - code_to_run = "\n".join([self_code, setup_code, interleaved, teardown_code]).strip() - else: - code_to_run = "\n".join([self_code, setup_code, source, teardown_code]).strip() - except Exception: - err = traceback.format_exc() - raise SphinxError("Problem with embed of " + path + ": \n" + str(err)) - else: - if indent > 0: - source = dedent(source) - if "interleave" in layout: - source = insert_output_start_stop_indicators(source) - code_to_run = source[:] - - # - # Run the code (if necessary) - # - skipped = failed = False - - if "output" in layout or "interleave" in layout or "plot" in layout: - - imports_not_required = "imports-not-required" in self.options - - if shows_plot: - # NOTE: import matplotlib AFTER __future__ (if it's there) - # All use of __future__ has been removed from OpenMDAO with v3.x - # so the related code has been removed here as well. - mpl_import = "\n".join( - [ - "import warnings", - "import matplotlib", - "warnings.filterwarnings('ignore')", - "matplotlib.use('Agg')\n", - ] - ) - code_to_run = mpl_import + code_to_run - - if "plot" in layout: - mpl_figure = '\nmatplotlib.pyplot.savefig("%s")' % plot_file_abs - code_to_run = code_to_run + mpl_figure - - if is_test and getattr(method, "__unittest_skip__", False): - skipped = True - failed = False - run_outputs = method.__unittest_skip_why__ - else: - skipped, failed, run_outputs = run_code( - code_to_run, - path, - module=module, - cls=class_, - imports_not_required=imports_not_required, - shows_plot=shows_plot, - ) - - # - # Handle output - # - if failed: - # Failed cases raised as a Directive warning (level 2 in docutils). - # This way, the sphinx build does not terminate if, for example, you are building on - # an environment where mpi or pyoptsparse are missing. - raise self.directive_error(2, run_outputs) - elif skipped: - # When building docs for a pull request, we do not want the build to fail due to not - # having SNOPT (since PRs will not have access to SNOPT). We want all other warnings. - TRAVIS_PR = os.environ.get("TRAVIS_PULL_REQUEST") - GITHUB_EV = os.environ.get("GITHUB_EVENT_NAME") - PR = (TRAVIS_PR and TRAVIS_PR != "false") or (GITHUB_EV and GITHUB_EV == "pull_request") - if not (PR and "pyoptsparse is not providing SNOPT" in run_outputs): - self.state_machine.reporter.warning(run_outputs) - - io_nodes = [get_skip_output_node(run_outputs)] - - else: - if "output" in layout: - output_blocks = run_outputs if isinstance(run_outputs, list) else [run_outputs] - - elif "interleave" in layout: - if is_test: - start = len(self_code) + len(setup_code) + len(mpl_import) - end = len(code_to_run) - len(teardown_code) - len(mpl_figure) - input_blocks = split_source_into_input_blocks(code_to_run[start:end]) - else: - input_blocks = split_source_into_input_blocks(code_to_run) - - output_blocks = extract_output_blocks(run_outputs) - - # Merge any input blocks for which there is no corresponding output - # with subsequent input blocks that do have output - input_blocks = consolidate_input_blocks(input_blocks, output_blocks) - - if "plot" in layout: - if not os.path.isfile(plot_file_abs): - raise SphinxError("Can't find plot file '%s'" % plot_file_abs) - - directive_dir = os.path.relpath(os.getcwd(), os.path.dirname(self.state.document.settings._source)) - # this filename must NOT contain an absolute path, else the Figure will not - # be able to find the image file in the generated html dir. - plot_file = os.path.join(directive_dir, plot_fname) - - # create plot node - fig = images.Figure( - self.name, - [plot_file], - self.options, - self.content, - self.lineno, - self.content_offset, - self.block_text, - self.state, - self.state_machine, - ) - plot_nodes = fig.run() - - # - # create a list of document nodes to return based on layout - # - doc_nodes = [] - skip_fail_shown = False - for opt in layout: - if opt == "code": - # we want the body of code to be formatted and code highlighted - body = nodes.literal_block(source, source) - body["language"] = "python" - doc_nodes.append(body) - elif skipped: - if not skip_fail_shown: - body = nodes.literal_block(source, source) - body["language"] = "python" - doc_nodes.append(body) - doc_nodes.extend(io_nodes) - skip_fail_shown = True - else: - if opt == "interleave": - doc_nodes.extend(get_interleaved_io_nodes(input_blocks, output_blocks)) - elif opt == "output": - doc_nodes.append(get_output_block_node(output_blocks)) - else: # plot - doc_nodes.extend(plot_nodes) - - return doc_nodes - - -def setup(app): - """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive("embed-code", EmbedCodeDirective) - node_setup(app) - - return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/doc/_exts/embed_compare.py b/doc/_exts/embed_compare.py deleted file mode 100644 index ecd9714a..00000000 --- a/doc/_exts/embed_compare.py +++ /dev/null @@ -1,146 +0,0 @@ -""" Sphinx directive for a side by side code comparison.""" - -from docutils import nodes - -import sphinx -from docutils.parsers.rst import Directive -import sys -import os - -this_directory = os.path.abspath(os.path.dirname(__file__)) -sys.path.insert(0, this_directory) - -from docutil_legacy import get_source_code - - -class ContentContainerDirective(Directive): - """ - Just for having an outer div. - - Relevant CSS: rosetta_outer - """ - - has_content = True - optional_arguments = 1 - - def run(self): - self.assert_has_content() - text = "\n".join(self.content) - node = nodes.container(text) - node["classes"].append("rosetta_outer") - - if self.arguments and self.arguments[0]: - node["classes"].append(self.arguments[0]) - - self.add_name(node) - self.state.nested_parse(self.content, self.content_offset, node) - return [node] - - -class EmbedCompareDirective(Directive): - """ - EmbedCompareDirective is a custom directive to allow blocks of - python code to be shown side by side to compare the new API with the old API. An - exmple looks like this: - - .. embed-compare:: - openmdao.test.whatever.method - optional text for searching for the first line - optional text for searching for the end line - optional style - - Old OpenMDAO lines of code go here. - - What the above will do is replace the directive and its args with the block of code - containing the class for method1 on the left and the class for method2 on the right. - - For optional styles, use 'style2' to use the alternate CSS style that has a light background on - both sides instead of red and green. Use 'no_compare' for straight code embedding without the - side-by-side comparison. (This is for pasting fragments of pre-tested code from a test.) - - Relevant CSS: rosetta_left and rosetta_right - """ - - # must have at least one directive for this to work - required_arguments = 1 - optional_arguments = 3 - has_content = True - - def run(self): - arg = self.arguments - compare = True - - # create a list of document nodes to return - doc_nodes = [] - - # Choose style - left_style = "rosetta_left" - right_style = "rosetta_right" - if len(arg) == 4: - if arg[3] == "style2": - left_style = "rosetta_left2" - right_style = "rosetta_right2" - elif arg[3] == "no_compare": - compare = False - - # LEFT side = Old OpenMDAO - if compare: - text = "\n".join(self.content) - left_body = nodes.literal_block(text, text) - left_body["language"] = "python" - left_body["classes"].append(left_style) - - # for RIGHT side, get the code block, and reduce it if requested - right_method = arg[0] - text, _, _, _, _ = get_source_code(right_method) - if len(arg) >= 3: - start_txt = arg[1] - end_txt = arg[2] - lines = text.split("\n") - - istart = 0 - for j, line in enumerate(lines): - if start_txt in line: - istart = j - break - - lines = lines[istart:] - iend = len(lines) - for j, line in enumerate(lines): - if end_txt in line: - iend = j + 1 - break - - lines = lines[:iend] - - # Remove the check suppression. - for j, line in enumerate(lines): - if "prob.setup(check=False" in line: - lines[j] = lines[j].replace("check=False, ", "") - lines[j] = lines[j].replace("check=False", "") - - # prune whitespace down to match first line - while lines[0].startswith(" "): - lines = [line[4:] for line in lines] - - text = "\n".join(lines) - - # RIGHT side = Current OpenMDAO - right_body = nodes.literal_block(text, text) - right_body["language"] = "python" - if compare: - right_body["classes"].append(right_style) - - if compare: - doc_nodes.append(left_body) - doc_nodes.append(right_body) - - return doc_nodes - - -def setup(app): - """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive("content-container", ContentContainerDirective) - app.add_directive("embed-compare", EmbedCompareDirective) - - return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/doc/_exts/embed_n2.py b/doc/_exts/embed_n2.py deleted file mode 100644 index 55184f8c..00000000 --- a/doc/_exts/embed_n2.py +++ /dev/null @@ -1,106 +0,0 @@ -from docutils import nodes -from docutils.statemachine import ViewList -from docutils.parsers.rst import Directive -import subprocess -import sphinx -from sphinx.util.nodes import nested_parse_with_titles -import os.path - - -class EmbedN2Directive(Directive): - """ - EmbedN2Directive is a custom directive to build and embed an N2 diagram into docs - An example usage would look like this: - - .. embed-n2:: - ../../examples/model.py - - What the above will do is replace the directive and its arg with an N2 diagram. - - The one required argument is the model file to be diagrammed. - Optional arguments are numerical width and height of the embedded object, and - "toolbar" if the toolbar should be visible by default. - - Example with width of 1500, height of 800, and toolbar displayed: - - .. embed-n2: - ../../examples/model.py - 1500 - 800 - toolbar - - """ - - required_arguments = 1 - optional_arguments = 3 - has_content = True - - def run(self): - path_to_model = self.arguments[0] - n2_dims = [1200, 700] - show_toolbar = False - - if len(self.arguments) > 1 and self.arguments[1]: - n2_dim_idx = 0 - for idx in range(1, len(self.arguments)): - if self.arguments[idx] == "toolbar": - show_toolbar = True - else: - n2_dims[n2_dim_idx] = self.arguments[idx] - n2_dim_idx = 1 - - np = os.path.normpath(os.path.join(os.getcwd(), path_to_model)) - - # check that the file exists - if not os.path.isfile(np): - raise IOError("File does not exist({0})".format(np)) - - # Generate N2 files into the target_dir. Those files are later copied - # into the top of the HTML hierarchy, so the HTML doc file needs a - # relative path to them. - target_dir = os.path.join(os.getcwd(), "_n2html") - - rel_dir = os.path.relpath(os.getcwd(), os.path.dirname(self.state.document.settings._source)) - html_base_name = os.path.basename(path_to_model).split(".")[0] + "_n2.html" - html_name = os.path.join(target_dir, html_base_name) - html_rel_name = os.path.join(rel_dir, html_base_name) - if show_toolbar: - html_rel_name += "#toolbar" - - cmd = subprocess.Popen(["openmdao", "n2", np, "--no_browser", "--embed", "-o" + html_name]) - cmd_out, cmd_err = cmd.communicate() - - rst = ViewList() - - # Add the content one line at a time. - # Second argument is the filename to report in any warnings - # or errors, third argument is the line number. - env = self.state.document.settings.env - docname = env.doc2path(env.docname) - - object_tag = ( - "" - ) - - rst.append(".. raw:: html", docname, self.lineno) - rst.append("", docname, self.lineno) # leave an empty line - rst.append(" %s" % object_tag, docname, self.lineno) - - # Create a node. - node = nodes.section() - - # Parse the rst. - nested_parse_with_titles(self.state, rst, node) - - # And return the result. - return node.children - - -def setup(app): - """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive("embed-n2", EmbedN2Directive) - - return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/doc/_exts/embed_options.py b/doc/_exts/embed_options.py deleted file mode 100644 index bc0f5a47..00000000 --- a/doc/_exts/embed_options.py +++ /dev/null @@ -1,69 +0,0 @@ -import importlib - -from docutils import nodes -from docutils.statemachine import ViewList - -import sphinx -from docutils.parsers.rst import Directive -from sphinx.util.nodes import nested_parse_with_titles -from openmdao.utils.options_dictionary import OptionsDictionary - - -class EmbedOptionsDirective(Directive): - """ - EmbedOptionsDirective is a custom directive to allow an OptionsDictionary - to be shown in a nice table form. An example usage would look like this: - - .. embed-options:: - openmdao.solvers.linear.petsc_ksp - PETScKrylov - options - - The 3 arguments are the module path, the class name, and name of the options dictionary. - - What the above will do is replace the directive and its args with a list of options - for the desired class. - - """ - - required_arguments = 3 - optional_arguments = 0 - has_content = True - - def run(self): - module_path, class_name, attribute_name = self.arguments - - mod = importlib.import_module(module_path) - klass = getattr(mod, class_name) - options = getattr(klass(), attribute_name) - - if not isinstance(options, OptionsDictionary): - raise TypeError("Object '%s' is not an OptionsDictionary." % attribute_name) - - lines = ViewList() - - n = 0 - for line in options.__rst__(): - lines.append(line, "options table", n) - n += 1 - - # Note applicable to System, Solver and Driver 'options', but not to 'recording_options' - if attribute_name != "recording_options": - lines.append("", "options table", n + 1) # Blank line required after table. - - # Create a node. - node = nodes.section() - node.document = self.state.document - - # Parse the rst. - nested_parse_with_titles(self.state, lines, node) - - # And return the result. - return node.children - - -def setup(app): - """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive("embed-options", EmbedOptionsDirective) - - return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/doc/_exts/embed_shell_cmd.py b/doc/_exts/embed_shell_cmd.py deleted file mode 100644 index 0717bb28..00000000 --- a/doc/_exts/embed_shell_cmd.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Sphinx directive to embed shell command output in the docs. - -The shell command is executed and the output is captured. -""" - -import sys -import os -from docutils import nodes -from docutils.parsers.rst.directives import unchanged - -import subprocess - -import sphinx -from docutils.parsers.rst import Directive -from sphinx.writers.html import HTMLTranslator -from sphinx.writers.html5 import HTML5Translator -from sphinx.errors import SphinxError - -import html as cgiesc - - -class failed_node(nodes.Element): - pass - - -def visit_failed_node(self, node): - pass - - -def depart_failed_node(self, node): - if not isinstance(self, (HTMLTranslator, HTML5Translator)): - self.body.append("output only available for HTML\n") - return - - html = """ -
-
-
-
{}
-
-
-
""".format( - node["text"] - ) - self.body.append(html) - - -class cmd_node(nodes.Element): - pass - - -def visit_cmd_node(self, node): - pass - - -def depart_cmd_node(self, node): - """ - This function creates the formatting that sets up the look of the blocks. - The look of the formatting is controlled by _theme/static/style.css - """ - if not isinstance(self, (HTMLTranslator, HTML5Translator)): - self.body.append("output only available for HTML\n") - return - - html = """ -
-
{}
-
""".format( - node["text"] - ) - - self.body.append(html) - - -class EmbedShellCmdDirective(Directive): - """ - EmbedShellCmdDirective is a custom directive to allow a shell command and the result - of running it to be shown in feature docs. - An example usage would look like this: - - .. embed-shell-cmd:: - :cmd: ls -ltr - - What the above will do is replace the directive and its args with the shell command, - run the command, and show the resulting output. - - """ - - # must have at least one arg (embedded test) for this to work - required_arguments = 0 - optional_arguments = 0 - has_content = False - - option_spec = { - "cmd": unchanged, # shell command to execute - "dir": unchanged, # working dir - "show_cmd": unchanged, # set this to make the shell command visible - "stderr": unchanged, # set this to include stderr contents with the output - } - - def run(self): - """ - Create a list of document nodes to return. - """ - if "cmd" in self.options: - cmdstr = self.options["cmd"] - cmd = cmdstr.split() - else: - raise SphinxError("'cmd' is not defined for embed-shell-cmd.") - - startdir = os.getcwd() - - if "dir" in self.options: - workdir = os.path.abspath(os.path.expandvars(os.path.expanduser(self.options["dir"]))) - else: - workdir = os.getcwd() - - if "stderr" in self.options: - stderr = subprocess.STDOUT - else: - stderr = None - - os.chdir(workdir) - - try: - output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, env=os.environ).decode("utf-8", "ignore") - except subprocess.CalledProcessError as err: - # Failed cases raised as a Directive warning (level 2 in docutils). - # This way, the sphinx build does not terminate if, for example, you are building on - # an environment where mpi or pyoptsparse are missing. - msg = "Running of embedded shell command '{}' in docs failed. Output was: \n{}".format( - cmdstr, err.output.decode("utf-8") - ) - raise self.directive_error(2, msg) - except Exception as err: - msg = "Running of embedded shell command '{}' in docs failed. Output was: \n{}".format(cmdstr, err) - raise self.directive_error(2, msg) - finally: - os.chdir(startdir) - - output = cgiesc.escape(output) - - show = True - if "show_cmd" in self.options: - show = self.options["show_cmd"].lower().strip() == "true" - - if show: - input_node = nodes.literal_block(cmdstr, cmdstr) - input_node["language"] = "none" - - output_node = cmd_node(text=output) - - if show: - return [input_node, output_node] - else: - return [output_node] - - -def setup(app): - """add custom directive into Sphinx so that it is found during document parsing""" - app.add_directive("embed-shell-cmd", EmbedShellCmdDirective) - app.add_node(failed_node, html=(visit_failed_node, depart_failed_node)) - app.add_node(cmd_node, html=(visit_cmd_node, depart_cmd_node)) - - return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/doc/_exts/link_class_from_docstring.py b/doc/_exts/link_class_from_docstring.py deleted file mode 100644 index 39f03f3d..00000000 --- a/doc/_exts/link_class_from_docstring.py +++ /dev/null @@ -1,87 +0,0 @@ -# a short sphinx extension to take care of hyperlinking in docstrings -# where a syntax of is employed. -import openmdao -import pkgutil -import inspect -import re -from openmdao.docs.config_params import IGNORE_LIST - -# first, we will need a dict that contains full pathnames to every class. -# we construct that here, once, then use it for lookups in om_process_docstring -package = openmdao - -om_classes = {} - - -def build_dict(): - global om_classes - for importer, modname, ispkg in pkgutil.walk_packages( - path=package.__path__, prefix=package.__name__ + ".", onerror=lambda x: None - ): - if not ispkg: - if "doc" not in modname: - if any(ignore in modname for ignore in IGNORE_LIST): - continue - module = importer.find_module(modname).load_module(modname) - for classname, class_object in inspect.getmembers(module, inspect.isclass): - if class_object.__module__.startswith("openmdao"): - om_classes[classname] = class_object.__module__ + "." + classname - - -def om_process_docstring(app, what, name, obj, options, lines): - """ - our process_docstring - """ - global om_classes - if not om_classes: - build_dict() - - for i in range(len(lines)): - # create a regex pattern to match - pat = r"(<.*?>)" - # find all matches of the pattern in a line - match = re.findall(pat, lines[i]) - if match: - for ma in match: - # strip off the angle brackets `<>` - m = ma[1:-1] - # to get rid of bad matches in OrderedDict.set_item - if m == "==": - continue - # if there's a dot in the pattern, it's a method - # e.g. - if "." in m: - # need to grab the class name and method name separately - split_match = m.split(".") - justclass = split_match[0] # class - justmeth = split_match[1] # method - if justclass in om_classes: - classfullpath = om_classes[justclass] - # construct a link :meth:`class.method ` - link = ":meth:`" + m + " <" + classfullpath + "." + justmeth + ">`" - # replace the text with the constructed line. - lines[i] = lines[i].replace(ma, link) - else: - # the class isn't in the class table! - print("WARNING: {} not found in dictionary of OpenMDAO methods".format(justclass)) - # replace instances of with just class in docstring - # (strip angle brackets) - lines[i] = lines[i].replace(ma, m) - # otherwise, it's a class - else: - if m in om_classes: - classfullpath = om_classes[m] - lines[i] = lines[i].replace(ma, ":class:`~" + classfullpath + "`") - else: - # the class isn't in the class table! - print("WARNING: {} not found in dictionary of OpenMDAO classes".format(m)) - # replace instances of with class in docstring - # (strip angle brackets) - lines[i] = lines[i].replace(ma, m) - - -# This is the crux of the extension--connecting an internal -# Sphinx event, "autodoc-process-docstring" with our own custom function. -def setup(app): - """ """ - app.connect("autodoc-process-docstring", om_process_docstring) diff --git a/doc/_exts/tags.py b/doc/_exts/tags.py deleted file mode 100644 index 75d2a5af..00000000 --- a/doc/_exts/tags.py +++ /dev/null @@ -1,80 +0,0 @@ -# tag.py, this custom Sphinx extension is activated in conf.py -# and allows the use of the custom directive for tags in our rst (e.g.): -# .. tags:: tag1, tag2, tag3 -from docutils.parsers.rst import Directive - -from docutils.parsers.rst.directives.admonitions import Admonition -from docutils import nodes -from sphinx.locale import _ - - -# The setup function for the Sphinx extension -def setup(app): - # This adds a new node class to build sys, with custom functs, (same name as file) - app.add_node(tag, html=(visit_tag_node, depart_tag_node)) - # This creates a new ".. tags:: " directive in Sphinx - app.add_directive("tags", TagDirective) - # These are event handlers, functions connected to events. - app.connect("doctree-resolved", process_tag_nodes) - app.connect("env-purge-doc", purge_tags) - # Identifies the version of our extension - return {"version": "0.1"} - - -def visit_tag_node(self, node): - self.visit_admonition(node) - - -def depart_tag_node(self, node): - self.depart_admonition(node) - - -def purge_tags(app, env, docname): - return - - -def process_tag_nodes(app, doctree, fromdocname): - env = app.builder.env - - -class tag(nodes.Admonition, nodes.Element): - pass - - -class TagDirective(Directive): - # This allows content in the directive, e.g. to list tags here - has_content = True - - def run(self): - env = self.state.document.settings.env - targetid = "tag-%d" % env.new_serialno("tag") - targetnode = nodes.target("", "", ids=[targetid]) - - # The tags fetched from the custom directive are one piece of text - # sitting in self.content[0] - taggs = self.content[0].split(", ") - links = [] - - for tagg in taggs: - # Create Sphinx doc refs of format :ref:`Tagname` - link = ":ref:`" + tagg + "<" + tagg + ">`" - links.append(link) - # Put links back in a single comma-separated string together - linkjoin = ", ".join(links) - - # Replace content[0] with hyperlinks to display in admonition - self.content[0] = linkjoin - - ad = Admonition( - self.name, - [_("Tags")], - self.options, - self.content, - self.lineno, - self.content_offset, - self.block_text, - self.state, - self.state_machine, - ) - - return [targetnode] + ad.run() diff --git a/doc/conf.py b/doc/conf.py index 896be85f..ce78d768 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -21,13 +21,10 @@ sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("..")) -sys.path.insert(0, os.path.abspath("./_exts")) # sphinx build needs to be able to find the openmdao embed_code plugin # so we add it to the path this_directory = os.path.abspath(os.path.dirname(__file__)) -# sys.path.insert(0, os.path.abspath(openconcept.__path__[0]+r'/doc/_exts')) - def generate_src_docs(dir, top, packages): """ diff --git a/openconcept/examples/B738_aerostructural.py b/openconcept/examples/B738_aerostructural.py index f8f8bf97..affe1e62 100644 --- a/openconcept/examples/B738_aerostructural.py +++ b/openconcept/examples/B738_aerostructural.py @@ -354,11 +354,11 @@ def configure_problem(num_nodes): return prob -def set_values(prob, num_nodes, range=2050): +def set_values(prob, num_nodes, mission_range=2050): # set some (required) mission parameters. Each phase needs a vertical and air-speed - # the entire mission needs a cruise altitude and range + # the entire mission needs a cruise altitude and mission range prob.set_val("cruise|h0", 35000.0, units="ft") - prob.set_val("mission_range", range, units="NM") + prob.set_val("mission_range", mission_range, units="NM") prob.set_val("climb.fltcond|vs", np.linspace(2000.0, 400.0, num_nodes), units="ft/min") prob.set_val("climb.fltcond|Ueas", np.linspace(220, 200, num_nodes), units="kn") prob.set_val("cruise.fltcond|vs", np.zeros((num_nodes,)), units="ft/min") diff --git a/openconcept/examples/minimal.py b/openconcept/examples/minimal.py index 0e16e6fb..6c50a291 100644 --- a/openconcept/examples/minimal.py +++ b/openconcept/examples/minimal.py @@ -145,14 +145,14 @@ def setup_problem(model=MissionAnalysis): axs = axs.flatten() # change 2x2 mtx of axes into 4-element vector # Define variables to plot - vars = [ + plot_vars = [ {"var": "fltcond|h", "name": "Altitude", "units": "ft"}, {"var": "fltcond|vs", "name": "Vertical speed", "units": "ft/min"}, {"var": "fltcond|Utrue", "name": "True airspeed", "units": "kn"}, {"var": "throttle", "name": "Throttle", "units": None}, ] - for idx_fig, var in enumerate(vars): + for idx_fig, var in enumerate(plot_vars): axs[idx_fig].set_xlabel("Range (nmi)") axs[idx_fig].set_ylabel(f"{var['name']}" if var["units"] is None else f"{var['name']} ({var['units']})") diff --git a/openconcept/examples/minimal_integrator.py b/openconcept/examples/minimal_integrator.py index abec2e15..a39db240 100644 --- a/openconcept/examples/minimal_integrator.py +++ b/openconcept/examples/minimal_integrator.py @@ -144,7 +144,7 @@ def setup(self): axs = axs.flatten() # change 2x3 mtx of axes into 4-element vector # Define variables to plot - vars = [ + plot_vars = [ {"var": "fltcond|h", "name": "Altitude", "units": "ft"}, {"var": "fltcond|vs", "name": "Vertical speed", "units": "ft/min"}, {"var": "fltcond|Utrue", "name": "True airspeed", "units": "kn"}, @@ -153,7 +153,7 @@ def setup(self): {"var": "weight", "name": "Weight", "units": "kg"}, ] - for idx_fig, var in enumerate(vars): + for idx_fig, var in enumerate(plot_vars): axs[idx_fig].set_xlabel("Range (nmi)") axs[idx_fig].set_ylabel(f"{var['name']}" if var["units"] is None else f"{var['name']} ({var['units']})") diff --git a/openconcept/utilities/math/multiply_divide_comp.py b/openconcept/utilities/math/multiply_divide_comp.py index 77f9fb9c..f70f5480 100644 --- a/openconcept/utilities/math/multiply_divide_comp.py +++ b/openconcept/utilities/math/multiply_divide_comp.py @@ -46,7 +46,7 @@ def __init__( scaling_factor=1, divide=None, input_units=None, - **kwargs + **kwargs, ): """ Allow user to create an multiplication system with one-liner. From 6ea79bf15e1e6c19875dd4245a78c2177681ed7d Mon Sep 17 00:00:00 2001 From: eytanadler Date: Thu, 11 Aug 2022 11:44:54 -0400 Subject: [PATCH 7/7] Cleaned up assertRaises in tests --- openconcept/mission/tests/test_trajectories.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openconcept/mission/tests/test_trajectories.py b/openconcept/mission/tests/test_trajectories.py index e12c5163..3c4d31dd 100644 --- a/openconcept/mission/tests/test_trajectories.py +++ b/openconcept/mission/tests/test_trajectories.py @@ -397,7 +397,7 @@ def setUp(self): self.p = om.Problem(model=self.TestPhase(num_nodes=self.nn)) def test_asserts(self): - with self.assertRaises(ValueError) as _: + with self.assertRaises(ValueError): self.p.setup(force_alloc_complex=True) @@ -569,7 +569,7 @@ def setUp(self): self.p = om.Problem(model=phase) def test_raises_error(self): - with self.assertRaises(NameError) as _: + with self.assertRaises(NameError): self.p.setup() @@ -932,7 +932,7 @@ def test_raises(self): traj.add_subsystem("phase1", PhaseForTrajTest(num_nodes=5)) - with self.assertRaises(ValueError) as _: + with self.assertRaises(ValueError): traj.link_phases("phase1", "phase2", states_to_skip=["b.ode_integ.f"])