From e6e0612f3b1b238e804b161efe7830fd3b39d428 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Mon, 17 Aug 2020 22:08:37 -0400 Subject: [PATCH 01/23] First dymos-based phase --- examples/B738_Dymos.py | 156 ++++++++ .../analysis/performance/dymos_phases.py | 366 ++++++++++++++++++ openconcept/analysis/trajectories.py | 7 +- openconcept/components/cfm56.py | 7 +- openconcept/utilities/dict_indepvarcomp.py | 62 ++- scratch/dymos_test.py | 264 +++++++++++++ 6 files changed, 856 insertions(+), 6 deletions(-) create mode 100644 examples/B738_Dymos.py create mode 100644 openconcept/analysis/performance/dymos_phases.py create mode 100644 scratch/dymos_test.py diff --git a/examples/B738_Dymos.py b/examples/B738_Dymos.py new file mode 100644 index 00000000..2d23ae3b --- /dev/null +++ b/examples/B738_Dymos.py @@ -0,0 +1,156 @@ +from __future__ import division +import sys +import os +import numpy as np + +sys.path.insert(0, os.getcwd()) +import openmdao.api as om +import openconcept.api as oc +# imports for the airplane model itself +from openconcept.analysis.aerodynamics import PolarDrag +from examples.aircraft_data.B738 import data as acdata +from openconcept.analysis.performance.mission_profiles import MissionWithReserve +from openconcept.components.cfm56 import CFM56 + +class B738AirplaneModel(oc.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) + + 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 + # propulsion_promotes_outputs = ['fuel_flow', 'thrust'] + 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={'value': 1.0*np.ones((nn,)), + 'units': 'kN'}, + thrust={'value': 1.0*np.ones((nn,)), + 'units': 'kN'}, + fuel_flow={'value': 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={'value': 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' + 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']) + + # generally the weights module will be custom to each airplane + passthru = om.ExecComp('OEW=x', + x={'value': 1.0, + 'units': 'kg'}, + OEW={'value': 1.0, + 'units': 'kg'}) + self.add_subsystem('OEW', passthru, + promotes_inputs=[('x', 'ac|weights|OEW')], + promotes_outputs=['OEW']) + + self.add_subsystem('weight', oc.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): + # 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', oc.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=['*']) + +# def configure_problem(): +# prob = om.Problem() +# prob.model = B738AnalysisGroup() +# 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) +# 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') + + + +if __name__ == "__main__": + run_738_analysis(plots=True) \ No newline at end of file diff --git a/openconcept/analysis/performance/dymos_phases.py b/openconcept/analysis/performance/dymos_phases.py new file mode 100644 index 00000000..c2664c7e --- /dev/null +++ b/openconcept/analysis/performance/dymos_phases.py @@ -0,0 +1,366 @@ +from __future__ import division +from openmdao.api import Group, ExplicitComponent, IndepVarComp, BalanceComp, ImplicitComponent +import openmdao.api as om +import openconcept.api as oc +from openconcept.analysis.atmospherics.compute_atmos_props import ComputeAtmosphericProperties +from openconcept.analysis.aerodynamics import Lift, StallSpeed +from openconcept.utilities.math import ElementMultiplyDivideComp, AddSubtractComp +from openconcept.utilities.math.integrals import Integrator +from openconcept.utilities.linearinterp import LinearInterpolator +from openconcept.utilities.math.integrals import Integrator +import numpy as np +import copy + +class Groundspeeds(ExplicitComponent): + """ + Computes groundspeed for vectorial true airspeed and true vertical speed. + + This is a helper function for the main mission analysis routines + and shouldn't be instantiated directly. + + Inputs + ------ + fltcond|vs : float + Vertical speed for all mission phases (vector, m/s) + fltcond|Utrue : float + True airspeed for all mission phases (vector, m/s) + + Outputs + ------- + fltcond|groundspeed : float + True groundspeed for all mission phases (vector, m/s) + fltcond|cosgamma : float + Cosine of the flght path angle for all mission phases (vector, dimensionless) + fltcond|singamma : float + Sine of the flight path angle for all mission phases (vector, dimensionless) + + Options + ------- + 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") + + 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)) + + 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) + 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'] + + def compute_partials(self, inputs, J): + 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 + +class HorizontalAcceleration(ExplicitComponent): + """ + Computes acceleration during takeoff run and effectively forms the T-D residual. + + Inputs + ------ + weight : float + Aircraft weight (scalar, kg) + drag : float + Aircraft drag at each analysis point (vector, N) + lift : float + Aircraft lift at each analysis point (vector, N) + thrust : float + Thrust at each TO analysis point (vector, N) + fltcond|singamma : float + The sine of the flight path angle gamma (vector, dimensionless) + braking : float + Effective rolling friction multiplier at each point (vector, dimensionless) + + Outputs + ------- + accel_horiz : float + Aircraft horizontal acceleration (vector, m/s**2) + + Options + ------- + num_nodes : int + Number of analysis points to run + """ + def initialize(self): + self.options.declare('num_nodes',default=1) + + def setup(self): + nn = self.options['num_nodes'] + g = 9.80665 #m/s^2 + 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=-g*np.ones((nn,))) + + + def compute(self, inputs, outputs): + nn = self.options['num_nodes'] + g = 9.80665 #m/s^2 + m = inputs['weight'] + floor_vec = np.where(np.less((g-inputs['lift']/m),0.0),0.0,1.0) + accel = inputs['thrust']/m - inputs['drag']/m - floor_vec*inputs['braking']*(g-inputs['lift']/m) - g*inputs['fltcond|singamma'] + outputs['accel_horiz'] = accel + + def compute_partials(self, inputs, J): + g = 9.80665 #m/s^2 + m = inputs['weight'] + floor_vec = np.where(np.less((g-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*(g-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 + + """ + Computes acceleration during takeoff run in the vertical plane. + Only used during full unsteady takeoff performance analysis due to stability issues + + Inputs + ------ + weight : float + Aircraft weight (scalar, kg) + drag : float + Aircraft drag at each analysis point (vector, N) + lift : float + Aircraft lift at each analysis point (vector, N) + thrust : float + Thrust at each TO analysis point (vector, N) + fltcond|singamma : float + The sine of the flight path angle gamma (vector, dimensionless) + fltcond|cosgamma : float + The sine of the flight path angle gamma (vector, dimensionless) + + Outputs + ------- + accel_vert : float + Aircraft horizontal acceleration (vector, m/s**2) + + Options + ------- + num_nodes : int + Number of analysis points to run + """ + def initialize(self): + self.options.declare('num_nodes',default=1) + + def setup(self): + nn = self.options['num_nodes'] + g = 9.80665 #m/s^2 + 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*g,lower=-1*g) + 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'] + g = 9.80665 #m/s^2 + cosg = inputs['fltcond|cosgamma'] + sing = inputs['fltcond|singamma'] + accel = (inputs['lift']*cosg + (inputs['thrust']-inputs['drag'])*sing - g*inputs['weight'])/inputs['weight'] + accel = np.clip(accel, -g, 2.5*g) + outputs['accel_vert'] = accel + + def compute_partials(self, inputs, J): + g = 9.80665 #m/s^2 + 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 + +class SteadyFlightCL(ExplicitComponent): + """ + Computes lift coefficient at each analysis point + + This is a helper function for the main mission analysis routine + and shouldn't be instantiated directly. + + Inputs + ------ + weight : float + Aircraft weight at each analysis point (vector, kg) + fltcond|q : float + Dynamic pressure at each analysis point (vector, Pascal) + ac|geom|wing|S_ref : float + Reference wing area (scalar, m**2) + fltcond|cosgamma : float + Cosine of the flght path angle for all mission phases (vector, dimensionless) + + Outputs + ------- + fltcond|CL : float + Lift coefficient (vector, dimensionless) + + Options + ------- + num_nodes : int + Number of analysis nodes to run + mission_segments : list + The list of mission segments to track + """ + 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('mission_segments',default=['climb','cruise','descent']) + def setup(self): + 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)) + + def compute(self, inputs, outputs): + g = 9.80665 #m/s^2 + outputs['fltcond|CL'] = inputs['fltcond|cosgamma']*g*inputs['weight']/inputs['fltcond|q']/inputs['ac|geom|wing|S_ref'] + + def compute_partials(self, inputs, J): + g = 9.80665 #m/s^2 + J['fltcond|CL','weight'] = inputs['fltcond|cosgamma']*g/inputs['fltcond|q']/inputs['ac|geom|wing|S_ref'] + J['fltcond|CL','fltcond|q'] = - inputs['fltcond|cosgamma']*g*inputs['weight'] / inputs['fltcond|q']**2 / inputs['ac|geom|wing|S_ref'] + J['fltcond|CL','ac|geom|wing|S_ref'] = - inputs['fltcond|cosgamma']*g*inputs['weight'] / inputs['fltcond|q'] / inputs['ac|geom|wing|S_ref']**2 + J['fltcond|CL','fltcond|cosgamma'] = g*inputs['weight']/inputs['fltcond|q']/inputs['ac|geom|wing|S_ref'] + +class DymosSteadyFlightODE(om.Group): + """ + This component group models steady flight conditions. + Settable mission parameters include: + Airspeed (fltcond|Ueas) + Vertical speed (fltcond|vs) + Duration of the segment (duration) + + Throttle is set automatically to ensure steady flight + + The BaseAircraftGroup object is passed in. + The BaseAircraftGroup should be built to accept the following inputs + and return the following outputs. + The outputs should be promoted to the top level in the component. + + Inputs + ------ + range : float + Total distance travelled (vector, m) + fltcond|h : float + Altitude (vector, m) + fltcond|vs : float + Vertical speed (vector, m/s) + fltcond|Ueas : float + Equivalent airspeed (vector, m/s) + fltcond|Utrue : float + True airspeed (vector, m/s) + fltcond|p : float + Pressure (vector, Pa) + fltcond|rho : float + Density (vector, kg/m3) + fltcond|T : float + Temperature (vector, K) + fltcond|q : float + Dynamic pressure (vector, Pa) + fltcond|CL : float + Lift coefficient (vector, dimensionless) + throttle : float + Motor / propeller throttle setting scaled from 0 to 1 or slightly more (vector, dimensionless) + propulsor_active : float + If a multi-propulsor airplane, a failure condition should be modeled in the propulsion model by multiplying throttle by propulsor_active. + It will generally be 1.0 unless a failure condition is being modeled, in which case it will be 0 (vector, dimensionless) + braking : float + Brake friction coefficient (default 0.4 for dry runway braking, 0.03 for resistance unbraked) + Should not be applied in the air or nonphysical effects will result (vector, dimensionless) + lift : float + Lift force (vector, N) + + Outputs + ------- + thrust : float + Total thrust force produced by all propulsors (vector, N) + drag : float + Total drag force in the airplane axis produced by all sources of drag (vector, N) + weight : float + Weight (mass, really) of the airplane at each point in time. (vector, kg) + ac|geom|wing|S_ref + Wing reference area (scalar, m**2) + ac|aero|CLmax_TO + CLmax with flaps in max takeoff position (scalar, dimensionless) + 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) + + def setup(self): + nn = self.options['num_nodes'] + ivcomp = self.add_subsystem('const_settings', IndepVarComp(), promotes_outputs=["*"]) + ivcomp.add_output('propulsor_active', val=np.ones(nn)) + ivcomp.add_output('braking', val=np.zeros(nn)) + # TODO feet fltcond|Ueas as control param + ivcomp.add_output('fltcond|Ueas',val=np.ones((nn,))*90, units='m/s') + # TODO feed fltcond|vs as control param + ivcomp.add_output('fltcond|vs',val=np.ones((nn,))*1, units='m/s') + ivcomp.add_output('zero_accel',val=np.zeros((nn,)),units='m/s**2') + + # TODO take out the integrator + integ = self.add_subsystem('ode_integ', 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') + # TODO Feed fltcond|h as state + + 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 + # TODO Can I promote up ac| quantities? + 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=['*']) + # TODO add range as a state + 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=2.0,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']) + # TODO still needs a Newton solver \ No newline at end of file diff --git a/openconcept/analysis/trajectories.py b/openconcept/analysis/trajectories.py index f585262f..3f5bf649 100644 --- a/openconcept/analysis/trajectories.py +++ b/openconcept/analysis/trajectories.py @@ -1,6 +1,7 @@ import openmdao.api as om import numpy as np from openconcept.utilities.math.integrals import Integrator +import dymos as dm import warnings # OpenConcept PhaseGroup will be used to hold analysis phases with time integration @@ -76,9 +77,11 @@ def _setup_procs(self, pathname, comm, mode, prob_meta): time_units = self._oc_time_units try: num_nodes = prob_meta['oc_num_nodes'] + self._under_dymos = False + self.add_subsystem('ode_integ', Integrator(time_setup='duration', method='simpson',diff_units=time_units, num_nodes=num_nodes)) except KeyError: - raise NameError('Integrator group must be created within an OpenConcept phase') - self.add_subsystem('ode_integ', Integrator(time_setup='duration', method='simpson',diff_units=time_units, num_nodes=num_nodes)) + # TODO this is a hack. best way would be to check if parent is instance of Dymos phase + self._under_dymos = True super(IntegratorGroup, self)._setup_procs(pathname, comm, mode, prob_meta) def _configure(self): diff --git a/openconcept/components/cfm56.py b/openconcept/components/cfm56.py index 547da256..68be2e95 100644 --- a/openconcept/components/cfm56.py +++ b/openconcept/components/cfm56.py @@ -48,7 +48,8 @@ def CFM56(num_nodes=1, plot=False): 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: - krigedata.append(np.array([throttle, altitude, mach, thrustijk.copy(), fuelburndata[ialt, jmach, kthrot].copy(), t4data[ialt, jmach, kthrot].copy()])) + 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()])) a = np.array(krigedata) comp = om.MetaModelUnStructuredComp(vec_size=num_nodes) @@ -79,7 +80,7 @@ def CFM56(num_nodes=1, plot=False): pred = np.zeros((25, 25, 3)) for i in range(25): for j in range(25): - prob['comp.throttle'] = 0.95 + prob['comp.throttle'] = 1.0 prob['comp.fltcond|h'] = alts[i,j] prob['comp.fltcond|M'] = machs[i,j] prob.run_model() @@ -90,7 +91,7 @@ def CFM56(num_nodes=1, plot=False): 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, levels=np.linspace(0.3,1.2,20)) + plt.contourf(machs, alts, (pred[:,:,1] / pred[:,:,0])*60*60) plt.colorbar() plt.figure() plt.xlabel('Mach') diff --git a/openconcept/utilities/dict_indepvarcomp.py b/openconcept/utilities/dict_indepvarcomp.py index 0c945937..833ae59b 100644 --- a/openconcept/utilities/dict_indepvarcomp.py +++ b/openconcept/utilities/dict_indepvarcomp.py @@ -50,7 +50,7 @@ def add_output_from_dict(self, structured_name, separator='|', **kwargs): Pipe symbols indicate treeing down one level Example 'aero:CLmax_flaps30' accesses data_dict['aero']['CLmax_flaps30'] separator : string - Separator to tree down into the data dict. Default ':' probably + Separator to tree down into the data dict. Default '|' probably shouldn't be overridden """ # tree down to the appropriate item in the tree @@ -72,3 +72,63 @@ def add_output_from_dict(self, structured_name, separator='|', **kwargs): super(DictIndepVarComp, self).add_output(name=structured_name, val=val, units=units, shape=val.shape) + +class DymosDesignParamsFromDict(): + r""" + Create Dymos parameters from an external file with a Python dictionary. + + + Attributes + ---------- + _data_dict : dict + A structured dictionary object with input data to read from. + """ + + def __init__(self, data_dict, dymos_traj): + """ + Initialize the component and store the data dictionary as an attribute. + + Parameters + ---------- + data_dict : dict + A structured dictionary object with input data to read from + dymos_traj : Dymos trajectory + A Dymos trajectory object with phases already added + """ + self._data_dict = data_dict + self._dymos_traj = dymos_traj + + 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 + + Parameters + ---------- + structured_name : string + A string matching the file structure in the dictionary object + Pipe symbols indicate treeing down one level + Example 'aero:CLmax_flaps30' accesses data_dict['aero']['CLmax_flaps30'] + separator : string + Separator to tree down into the data dict. Default '|' probably + shouldn't be overridden + """ + # tree down to the appropriate item in the tree + split_names = structured_name.split(separator) + data_dict_tmp = self._data_dict + for sub_name in split_names: + try: + data_dict_tmp = data_dict_tmp[sub_name] + except KeyError: + raise KeyError('"%s" does not exist in the data dictionary' % structured_name) + try: + 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) + + if isinstance(val, numbers.Number): + val = np.array([val]) + + 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 diff --git a/scratch/dymos_test.py b/scratch/dymos_test.py new file mode 100644 index 00000000..97cc053c --- /dev/null +++ b/scratch/dymos_test.py @@ -0,0 +1,264 @@ +from __future__ import division +from openmdao.api import Group, ExplicitComponent, IndepVarComp, BalanceComp, ImplicitComponent +import openmdao.api as om +import openconcept.api as oc +from openconcept.analysis.atmospherics.compute_atmos_props import ComputeAtmosphericProperties +from openconcept.analysis.aerodynamics import Lift +from openconcept.utilities.math import ElementMultiplyDivideComp, AddSubtractComp +from openconcept.utilities.math.integrals import Integrator +from openconcept.utilities.linearinterp import LinearInterpolator +from openconcept.utilities.math.integrals import Integrator +from openconcept.utilities.dict_indepvarcomp import DymosDesignParamsFromDict +from openconcept.analysis.performance.solver_phases import Groundspeeds, SteadyFlightCL, HorizontalAcceleration +import numpy as np +import copy +import dymos as dm +import matplotlib +import matplotlib.pyplot as plt +from examples.aircraft_data.B738 import data as b738data +from examples.B738 import B738AirplaneModel + +# TODO make a DymosODE group +# Configure method will iterate down and find tags +# Setup_procs method will push DymosODE metadata down +# where to invoke the add_state in the call stack? + +class DymosSteadyFlightODE(om.Group): + """ + Test + """ + def initialize(self): + self.options.declare('num_nodes',default=1) + self.options.declare('aircraft_model') + self.options.declare('flight_phase', default='cruise') + + def setup(self): + nn = self.options['num_nodes'] + 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.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=['*']) + + +def extract_states_from_airplane(acmodel): + pass + +if __name__ == "__main__": + + # + # Define the OpenMDAO problem + # + p = om.Problem(model=om.Group()) + + # + # Define a Trajectory object + # + traj = dm.Trajectory() + p.model.add_subsystem('traj', subsys=traj) + + # + # Define a Dymos Phase object with GaussLobatto Transcription + # + + odekwargs = {'aircraft_model': B738AirplaneModel} + phase0 = dm.Phase(ode_class=DymosSteadyFlightODE, ode_init_kwargs=odekwargs, + transcription=dm.Radau(num_segments=11, order=3, solve_segments=True)) + + traj.add_phase(name='phase0', phase=phase0) + # traj.add_phase(name='phase1', phase=phase1) + acparams = DymosDesignParamsFromDict(b738data, traj) + acparams.add_output_from_dict('ac|aero|polar|e') + acparams.add_output_from_dict('ac|aero|polar|CD0_cruise') + + acparams.add_output_from_dict('ac|geom|wing|S_ref') + acparams.add_output_from_dict('ac|geom|wing|AR') + + acparams.add_output_from_dict('ac|weights|MTOW') + acparams.add_output_from_dict('ac|weights|OEW') + + # + # Set the time options + # Time has no targets in our ODE. + # We fix the initial time so that the it is not a design variable in the optimization. + # The duration of the phase is allowed to be optimized, but is bounded on [0.5, 10]. + # + phase0.set_time_options(fix_initial=True, duration_bounds=(100, 10000), units='s', duration_ref=100., initial_ref0=0.0, initial_ref=1.0) + # phase1.set_time_options(fix_initial=False, duration_bounds=(50, 10000), units='s') + # + # Set the time options + # Initial values of positions and velocity are all fixed. + # The final value of position are fixed, but the final velocity is a free variable. + # The equations of motion are not functions of position, so 'x' and 'y' have no targets. + # The rate source points to the output in the ODE which provides the time derivative of the + # given state. + + + # auto add these + phase0.add_control(name='throttle', units=None, lower=0.0, upper=1.00, targets=['throttle'], ref=1.0) + phase0.add_path_constraint('accel_horiz', lower=0.0, ref=1.) + phase0.add_control(name='fltcond|vs', units='ft/min', lower=400, upper=7000, targets=['fltcond|vs'], opt=False, ref=3000.) + phase0.add_state('fltcond|h', fix_initial=True, fix_final=False, units='km', rate_source='fltcond|vs', targets=['fltcond|h'], ref=10., defect_ref=10.) + phase0.add_boundary_constraint('fltcond|h', loc='final', units='ft', equals=33000., ref=33000.) + phase0.add_control(name='fltcond|Ueas', units='kn', lower=180, upper=250, targets=['fltcond|Ueas'], opt=False, ref=250.) + phase0.add_state('range', fix_initial=True, fix_final=False, units='km', rate_source='fltcond|groundspeed', ref=100., defect_ref=100.) + + # add states for the temperatures + # add states for the battery SOC + # add a control for Tc and Th set + # add a control for duct exit area (with limits) + # add a path constraint Tmotor < 90C > -10C + # add a path constraint Tbattery <70C > 0C + + # custom state + phase0.add_state('fuel_used', fix_initial=True, fix_final=False, units='kg', rate_source='fuel_flow', targets=['fuel_used'], ref=1000., defect_ref=1000.) + # need to know + # rate source location + # target location + # initial condition + # scaler + # defect scaler + # unit + + # phase1.add_control(name='throttle', units=None, lower=0.0, upper=1.5, targets=['throttle']) + # phase1.add_path_constraint('accel_horiz', equals=0.0) + # phase1.add_control(name='fltcond|vs', units='m/s', lower=0, upper=10, targets=['fltcond|vs']) + # phase1.add_state('fltcond|h', fix_initial=True, fix_final=True, units='km', rate_source='fltcond|vs', targets=['fltcond|h']) + # phase1.add_control(name='fltcond|Ueas', units='kn', lower=180, upper=250, targets=['fltcond|Ueas']) + # phase1.add_state('range', fix_initial=False, fix_final=False, units='km', rate_source='fltcond|groundspeed') + # phase1.add_state('fuel_used', fix_initial=False, fix_final=False, units='kg', lower=0.0, rate_source='fuel_flow', targets=['fuel_used']) + + # traj.link_phases(['phase0','phase1']) + # traj.add_design_parameter('ac|weights|MTOW', units='kg', val=500., opt=False, targets={'phase0':['ac|weights|MTOW']}, dynamic=False) + # Minimize final time. + phase0.add_objective('weight', loc='final', ref=-1000.) + # phase0.add_boundary_constraint('time', loc='final', units='s', upper=800.) + + + # Set the driver. + p.driver = om.pyOptSparseDriver() + p.driver.options['optimizer'] = 'SNOPT' + # Allow OpenMDAO to automatically determine our sparsity pattern. + # Doing so can significant speed up the execution of Dymos. + p.driver.declare_coloring() + + # p.model.promotes('traj', inputs=['phase*.rhs_all.ac|*']) + + # Setup the problem + + + p.setup(check=True) + + # Now that the OpenMDAO problem is setup, we can set the values of the states. + p['traj.phase0.t_initial'] = 1.0 + p['traj.phase0.t_duration'] = 900.0 + p.set_val('traj.phase0.states:fltcond|h', + phase0.interpolate(ys=[0, 33000], nodes='state_input'), + units='ft') + + p.set_val('traj.phase0.states:range', + phase0.interpolate(ys=[0, 80], nodes='state_input'), + units='km') + + p.set_val('traj.phase0.states:fuel_used', + phase0.interpolate(ys=[0, 1000], nodes='state_input'), + units='kg') + + p.set_val('traj.phase0.controls:fltcond|Ueas', + phase0.interpolate(ys=[230, 220], nodes='control_input'), + units='kn') + + p.set_val('traj.phase0.controls:fltcond|vs', + phase0.interpolate(ys=[2300, 600], nodes='control_input'), + units='ft/min') + + p.set_val('traj.phase0.controls:throttle', + phase0.interpolate(ys=[0.4, 0.8], nodes='control_input'), + units=None) + # p.set_val('traj.phase1.states:fltcond|h', + # phase1.interpolate(ys=[25000, 27000], nodes='state_input'), + # units='ft') + + # p.set_val('traj.phase1.states:range', + # phase1.interpolate(ys=[50, 60], nodes='state_input'), + # units='km') + + # p.set_val('traj.phase1.states:fuel_used', + # phase1.interpolate(ys=[500, 1000], nodes='state_input'), + # units='kg') + + # p.set_val('traj.phase1.controls:fltcond|Ueas', + # phase1.interpolate(ys=[180, 180], nodes='control_input'), + # units='kn') + + # p.set_val('traj.phase1.controls:fltcond|vs', + # phase1.interpolate(ys=[0, 0], nodes='control_input'), + # units='ft/s') + + # Run the driver to solve the problem + # p['traj.phases.phase0.initial_conditions.initial_value:range'] = 100. + p.run_driver() + + # Check the validity of our results by using scipy.integrate.solve_ivp to + # integrate the solution. + sim_out = traj.simulate() + + # Plot the results + fig, axes = plt.subplots(nrows=1, ncols=4, figsize=(12, 4.5)) + + axes[0].plot(p.get_val('traj.phase0.timeseries.states:range'), + p.get_val('traj.phase0.timeseries.states:fltcond|h'), + 'ro', label='solution') + + axes[0].plot(sim_out.get_val('traj.phase0.timeseries.states:range'), + sim_out.get_val('traj.phase0.timeseries.states:fltcond|h'), + 'b-', label='simulation') + + axes[0].set_xlabel('range (km)') + axes[0].set_ylabel('alt (km)') + axes[0].legend() + axes[0].grid() + + axes[1].plot(p.get_val('traj.phase0.timeseries.time'), + p.get_val('traj.phase0.timeseries.controls:fltcond|Ueas', units='kn'), + 'ro', label='solution') + + axes[1].plot(sim_out.get_val('traj.phase0.timeseries.time'), + sim_out.get_val('traj.phase0.timeseries.controls:fltcond|Ueas', units='kn'), + 'b-', label='simulation') + + axes[1].set_xlabel('time (s)') + axes[1].set_ylabel(r'Command speed (kn)') + axes[1].legend() + axes[1].grid() + + axes[2].plot(p.get_val('traj.phase0.timeseries.time'), + p.get_val('traj.phase0.timeseries.controls:fltcond|vs', units='ft/min'), + 'ro', label='solution') + + axes[2].plot(sim_out.get_val('traj.phase0.timeseries.time'), + sim_out.get_val('traj.phase0.timeseries.controls:fltcond|vs', units='ft/min'), + 'b-', label='simulation') + + axes[2].set_xlabel('time (s)') + axes[2].set_ylabel(r'Command climb rate (ft/min)') + axes[2].legend() + axes[2].grid() + + axes[3].plot(p.get_val('traj.phase0.timeseries.time'), + p.get_val('traj.phase0.timeseries.controls:throttle', units=None), + 'ro', label='solution') + + axes[3].plot(sim_out.get_val('traj.phase0.timeseries.time'), + sim_out.get_val('traj.phase0.timeseries.controls:throttle', units=None), + 'b-', label='simulation') + + axes[3].set_xlabel('time (s)') + axes[3].set_ylabel(r'Throttle') + axes[3].legend() + axes[3].grid() + + plt.show() + # p.check_partials(compact_print=True) + p.model.list_inputs(print_arrays=False, units=True) \ No newline at end of file From d248305b266eb77576e67486f4b811d13af50569 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Mon, 17 Aug 2020 22:10:41 -0400 Subject: [PATCH 02/23] add hybrid engine surrogate model --- openconcept/components/N3hybrid.py | 152 +++++++ openconcept/components/N3opt.py | 381 ++++++++++++++++++ .../empirical_data/n+3/power_off/SMN.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_off/SMW.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_off/alt.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_off/mach.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_off/t4.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_off/throttle.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_off/thrust.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_off/wf.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_1MW/SMN.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_1MW/SMW.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_1MW/alt.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_1MW/mach.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_1MW/t4.npy | Bin 0 -> 5056 bytes .../n+3/power_on_1MW/throttle.npy | Bin 0 -> 5056 bytes .../n+3/power_on_1MW/thrust.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_1MW/wf.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_500kW/SMN.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_500kW/SMW.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_500kW/alt.npy | Bin 0 -> 5056 bytes .../n+3/power_on_500kW/mach.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_500kW/t4.npy | Bin 0 -> 5056 bytes .../n+3/power_on_500kW/throttle.npy | Bin 0 -> 5056 bytes .../n+3/power_on_500kW/thrust.npy | Bin 0 -> 5056 bytes .../empirical_data/n+3/power_on_500kW/wf.npy | Bin 0 -> 5056 bytes 26 files changed, 533 insertions(+) create mode 100644 openconcept/components/N3hybrid.py create mode 100644 openconcept/components/N3opt.py create mode 100644 openconcept/components/empirical_data/n+3/power_off/SMN.npy create mode 100644 openconcept/components/empirical_data/n+3/power_off/SMW.npy create mode 100644 openconcept/components/empirical_data/n+3/power_off/alt.npy create mode 100644 openconcept/components/empirical_data/n+3/power_off/mach.npy create mode 100644 openconcept/components/empirical_data/n+3/power_off/t4.npy create mode 100644 openconcept/components/empirical_data/n+3/power_off/throttle.npy create mode 100644 openconcept/components/empirical_data/n+3/power_off/thrust.npy create mode 100644 openconcept/components/empirical_data/n+3/power_off/wf.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_1MW/SMN.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_1MW/SMW.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_1MW/alt.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_1MW/mach.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_1MW/t4.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_1MW/throttle.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_1MW/thrust.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_1MW/wf.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_500kW/SMN.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_500kW/SMW.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_500kW/alt.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_500kW/mach.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_500kW/t4.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_500kW/throttle.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_500kW/thrust.npy create mode 100644 openconcept/components/empirical_data/n+3/power_on_500kW/wf.npy diff --git a/openconcept/components/N3hybrid.py b/openconcept/components/N3hybrid.py new file mode 100644 index 00000000..fe657fe5 --- /dev/null +++ b/openconcept/components/N3hybrid.py @@ -0,0 +1,152 @@ +from __future__ import division +import numpy as np +import openmdao.api as om +import openconcept +from openconcept.utilities.surrogates.cached_kriging_surrogate import KrigingSurrogate + +def N3Opt(num_nodes=1, plot=False): + """ + A geared turbofan based on NASA's N+3 architecture + + Inputs + ------ + throttle: float + Engine throttle. Controls power and fuel flow. + Produces 100% of rated power at throttle = 1. + Should be in range 0 to 1 or slightly above 1. + (vector, dimensionless) + fltcond|h: float + Altitude + (vector, dimensionless) + fltcond|M: float + Mach number + (vector, dimensionless) + + Outputs + ------- + thrust : float + Thrust developed by the engine (vector, lbf) + fuel_flow : float + Fuel flow consumed (vector, lbm/s) + T4 : float + Turbine inlet temperature (vector, Rankine) + + Options + ------- + num_nodes : int + Number of analysis points to run (sets vec length; default 1) + """ + file_root = openconcept.__path__[0] + r'/components/empirical_data/n+3/' + thrustdata = np.load(file_root + r'/power_on_500kW/thrust.npy') + fuelburndata = np.load(file_root + r'/power_on_500kW/wf.npy') + t4data = np.load(file_root + r'/power_on_500kW/t4.npy') + altdata = np.load(file_root + r'/power_on_500kW/alt.npy') + machdata = np.load(file_root + r'/power_on_500kW/mach.npy') + throttledata = np.load(file_root + r'/power_on_500kW/throttle.npy') + + krigedata = [] + for ialt in range(8): + for jmach in range(7): + for kthrot in range(11): + thrustijk = thrustdata[ialt, jmach, kthrot] + if thrustijk > 0.0: + krigedata.append(np.array([throttledata[ialt, jmach, kthrot].copy(), + altdata[ialt, jmach, kthrot].copy(), + machdata[ialt, jmach ,kthrot].copy(), + 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_output('thrust', np.ones((num_nodes,))*10000., + training_data=a[:,3], units='lbf', + surrogate=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3thrust.pkl')) + comp.add_output('fuel_flow', np.ones((num_nodes,))*3.0, + training_data=a[:,4], units='lbm/s', + surrogate=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3fuelburn.pkl')) + comp.add_output('T4', np.ones((num_nodes,))*3000., + training_data=a[:,5], units='R', + surrogate=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3T4.pkl')) + comp.options['default_surrogate'] = KrigingSurrogate(lapack_driver='gesvd', cache_trained_model=True) + + if plot: + import matplotlib.pyplot as plt + prob = om.Problem() + prob.model.add_subsystem('comp', comp) + prob.setup() + + machs = np.linspace(0.2, 0.8, 25) + alts = np.linspace(0.0, 35000., 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.run_model() + 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[:,:,0]) + 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[:,:,0]) + 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]) + plt.contourf(machs, alts, pred[:,:,0], levels=20) + plt.colorbar() + plt.show() + + throttles = np.linspace(0.05, 1.0, 25) + alts = np.linspace(0.0, 35000., 25) + throttles, alts = np.meshgrid(throttles, alts) + pred = np.zeros((25, 25, 3)) + for i in range(25): + for j in range(25): + prob['comp.throttle'] = throttles[i,j] + prob['comp.fltcond|h'] = alts[i,j] + prob['comp.fltcond|M'] = 0.3 + prob.run_model() + 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(machs, alts, pred[:,:,0]) + 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[:,:,0]) + 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]) + plt.contourf(throttles, alts, pred[:,:,0], levels=20) + plt.colorbar() + plt.show() + return comp diff --git a/openconcept/components/N3opt.py b/openconcept/components/N3opt.py new file mode 100644 index 00000000..93c60e74 --- /dev/null +++ b/openconcept/components/N3opt.py @@ -0,0 +1,381 @@ +from __future__ import division +import numpy as np +import openmdao.api as om +import openconcept +from openconcept.utilities.surrogates.cached_kriging_surrogate import KrigingSurrogate + +def N3Opt(num_nodes=1, plot=False): + """ + A geared turbofan based on NASA's N+3 architecture + + Inputs + ------ + throttle: float + Engine throttle. Controls power and fuel flow. + Produces 100% of rated power at throttle = 1. + Should be in range 0 to 1 or slightly above 1. + (vector, dimensionless) + fltcond|h: float + Altitude + (vector, dimensionless) + fltcond|M: float + Mach number + (vector, dimensionless) + + Outputs + ------- + thrust : float + Thrust developed by the engine (vector, lbf) + fuel_flow : float + Fuel flow consumed (vector, lbm/s) + T4 : float + Turbine inlet temperature (vector, Rankine) + + Options + ------- + num_nodes : int + Number of analysis points to run (sets vec length; default 1) + """ + file_root = openconcept.__path__[0] + r'/components/empirical_data/n+3/' + thrustdata = np.load(file_root + r'/power_off/thrust.npy') + fuelburndata = np.load(file_root + r'/power_off/wf.npy') + t4data = np.load(file_root + r'/power_off/t4.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): + for jmach in range(7): + for kthrot in range(11): + thrustijk = thrustdata[ialt, jmach, kthrot] + if thrustijk > 0.0: + krigedata.append(np.array([throttledata[ialt, jmach, kthrot].copy(), + altdata[ialt, jmach, kthrot].copy(), + machdata[ialt, jmach ,kthrot].copy(), + 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_output('thrust', np.ones((num_nodes,))*10000., + training_data=a[:,3], units='lbf', + surrogate=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3_poweroff_thrust.pkl')) + comp.add_output('fuel_flow', np.ones((num_nodes,))*3.0, + training_data=a[:,4], units='lbm/s', + surrogate=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3_poweroff_fuelburn.pkl')) + # comp.add_output('T4', np.ones((num_nodes,))*3000., + # training_data=a[:,5], units='R', + # surrogate=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3_poweroff_T4.pkl')) + comp.options['default_surrogate'] = KrigingSurrogate(lapack_driver='gesvd', cache_trained_model=True) + + if plot: + import matplotlib.pyplot as plt + prob = om.Problem() + prob.model.add_subsystem('comp', comp) + prob.setup() + + machs = np.linspace(0.2, 0.8, 25) + alts = np.linspace(0.0, 35000., 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.run_model() + 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[:,:,0]) + 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[:,:,0]) + 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]) + plt.contourf(machs, alts, pred[:,:,0], levels=20) + plt.colorbar() + plt.show() + + throttles = np.linspace(0.00, 1.0, 25) + alts = np.linspace(0.0, 35000., 25) + throttles, alts = np.meshgrid(throttles, alts) + pred = np.zeros((25, 25, 3)) + for i in range(25): + for j in range(25): + prob['comp.throttle'] = throttles[i,j] + prob['comp.fltcond|h'] = alts[i,j] + prob['comp.fltcond|M'] = 0.3 + prob.run_model() + 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(machs, alts, pred[:,:,0]) + 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[:,:,0]) + 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]) + plt.contourf(throttles, alts, pred[:,:,0], levels=20) + plt.colorbar() + plt.show() + return comp + +def N3Hybrid(num_nodes=1, plot=False): + """ + Computes fuel savings attributable to hybridization + + Inputs + ------ + throttle: float + Engine throttle. Controls power and fuel flow. + Produces 100% of rated power at throttle = 1. + Should be in range 0 to 1 or slightly above 1. + (vector, dimensionless) + fltcond|h: float + Altitude + (vector, dimensionless) + fltcond|M: float + Mach number + (vector, dimensionless) + + Outputs + ------- + thrust : float + Thrust developed by the engine (vector, lbf) + fuel_flow : float + Fuel flow consumed (vector, lbm/s) + T4 : float + Turbine inlet temperature (vector, Rankine) + + Options + ------- + num_nodes : int + Number of analysis points to run (sets vec length; default 1) + """ + file_root = openconcept.__path__[0] + r'/components/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') + + 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') + + krigedata = [] + # do the base case + for ialt in range(8): + for jmach in range(7): + 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()])) + # 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()])) + + # do the 1MW case + for ialt in range(8): + for jmach in range(7): + 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()])) + + 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=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3_hybrid_thrust.pkl')) + comp.add_output('fuel_flow', np.ones((num_nodes,))*3.0, + training_data=a[:,5], units='lbm/s', + surrogate=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3_hybrid_fuelflow.pkl')) + comp.add_output('surge_margin', np.ones((num_nodes,))*3.0, + training_data=a[:,6], units=None, + surrogate=KrigingSurrogate(cache_trained_model=True, cached_model_filename='n3_hybrid_smw.pkl')) + comp.options['default_surrogate'] = KrigingSurrogate(lapack_driver='gesvd', cache_trained_model=True) + + if plot: + import matplotlib.pyplot as plt + prob = om.Problem() + prob.model.add_subsystem('comp', comp) + prob.setup() + + machs = np.linspace(0.2, 0.8, 25) + alts = np.linspace(0.0, 35000., 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.run_model() + 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[:,:,0]) + 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[:,:,0]) + 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]) + 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) + 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.run_model() + 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(machs, alts, pred[:,:,0]) + 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[:,:,0]) + 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]) + plt.contourf(throttles, alts, pred[:,:,0], levels=20) + plt.colorbar() + plt.show() + + powers = np.linspace(0, 1000, 25) + throttles = np.linspace(0.3, 1.0, 25) + powers, throttles = np.meshgrid(powers, throttles) + 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.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() + plt.figure() + 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.colorbar() + plt.figure() + 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.colorbar() + plt.figure() + 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.colorbar() + plt.figure() + 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.colorbar() + plt.show() + return comp \ No newline at end of file diff --git a/openconcept/components/empirical_data/n+3/power_off/SMN.npy b/openconcept/components/empirical_data/n+3/power_off/SMN.npy new file mode 100644 index 0000000000000000000000000000000000000000..4466253f9a7268dfb3a1ec52073430ae545d5462 GIT binary patch literal 5056 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 T`9p1*+T|F`ywUOuQl0?-FGwDk literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_off/t4.npy b/openconcept/components/empirical_data/n+3/power_off/t4.npy new file mode 100644 index 0000000000000000000000000000000000000000..03eae47c7cb60bb791176f5d5c9219b5ab89dba7 GIT binary patch literal 5056 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 OlQ`edUc%S&5B~$~TG&qj literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_1MW/SMN.npy b/openconcept/components/empirical_data/n+3/power_on_1MW/SMN.npy new file mode 100644 index 0000000000000000000000000000000000000000..e28d90e77b3fbf992ffdef1873a4efe83c7b0d27 GIT binary patch literal 5056 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 Wf4+!jEw|^tGgP=~_p6;?<9`5D*{}5g literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_1MW/SMW.npy b/openconcept/components/empirical_data/n+3/power_on_1MW/SMW.npy new file mode 100644 index 0000000000000000000000000000000000000000..ac2504ec7ba4ffb662c00580e4272ebd901418ec GIT binary patch literal 5056 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 literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_1MW/t4.npy b/openconcept/components/empirical_data/n+3/power_on_1MW/t4.npy new file mode 100644 index 0000000000000000000000000000000000000000..00a10f85cae777a8cd2563ba0825504e03b7552f GIT binary patch literal 5056 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 HrsKZ=S1eiz literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_1MW/throttle.npy b/openconcept/components/empirical_data/n+3/power_on_1MW/throttle.npy new file mode 100644 index 0000000000000000000000000000000000000000..16037118202e80d0fec8933f454723a05ec266b3 GIT binary patch literal 5056 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 KtdSZ8H{-vYe445N literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_1MW/wf.npy b/openconcept/components/empirical_data/n+3/power_on_1MW/wf.npy new file mode 100644 index 0000000000000000000000000000000000000000..b676e428da12439e7365c369168ce99508dd9758 GIT binary patch literal 5056 zcmb`KX*kq-*vE%xmnBN2D3wT5%2M$yDO$8Rwn9#_M3J>5TjCH&h>mbj6eUZtwvc@( z+t{~ZjCCwC#=buDT-RxOao#=G^Lz8Z{=XNW>;B#M{k@mq3tH-zuCNjA5*~;do7mm3 z6+0p#cFa^>Oj1P5^p35)?R6`|JGRCq|9gM>`fWQC=Dpp`>((aB|2^`OB65-<2M&l! zinxgUpU&zkLEGzH@;%Tz*`r0w?}Wp1lDm&_Gw7SLQ`7I(fZ?@r!zG1$r1HM0n~hEe z;otLZ+s3!~wdK&cCQgoYs7D9aIhBU1?Xa>)Ay3 zYhs$(h1VtXU9KcDUab$`IV#o;W6|d2`hoS3|KsdYiOL@s$s-ssx<3EQqvigx*FY$? z#5%GF6>As1)0b*c5lwHIP;7>xk5*PpbSGl%=bIL%y3m+In71=-#cd98y51}i3FE=9 z*T|Pc!A`Z{@RqFqaCcdz1?XNWz>}|uXO$jTgW=n7xzxWAUo2$LG&zvqxK-{t`|&Ou zHMf~5X={d|QfTA(C$(51p&PfqvIOq!jb3%PKmCU*`0dV2yUcg+>5p6J7*}H2j$p*c z-2l{?hk?R6p4Luc5KGvr-_oWV=cfBP4DQJV)gNTdL`kgR|Pm|&>ZN#S0OSiVpRbh?u z{iXhO1(>xOPfXpK@E@*cOG_{zIU5}nd9tT(mVx)q;$yNK5tjwi1Df(%;jHrBO{;kT z@5~S|5>0_$^n}jG=OoZ$14A8FG2azvlOnlTg3%c>ihft-zkCAWNK|l=sB{f_2~9UT z>sxTRW5>#!5@Zy`4y%M6?1M6gah+D`9D##cOT@R+-R=-hN(FdzY z&9>)@T~NJBzqx9n1)HfZ@mDlzA!BuUty^Y3KB)MX5*>c)LiX~~>+}^rAjlioOo(lO zRR54UQJ;i4mmve=+Z3pAnJ?Hc@5QKF?ZWf`&^gk=Aaonz5Gsra$ zVt3v@{B-8GE>><$BR{xOhQ(z^E)%JB*zj^!{7$oW^sgm@F&E{_>x=B|kaQs!~>rbyK3JLv@GA*xQuiE)kV%6*<56Q zA;JvSn{W_o@U_Q-n`c!E7`;L(%}ToOwkTBX^Ugey}%7^>_`P23H>cOKxYT>@q ziNsvz-JSfscwd^MCq|~?RPmIm>9t|_8ztZ5vg(8QvtFwzY8Ni9Ni`T;*$n5-Quis_ z3f#7~3*%q&%g+Qtz07u*EiZ^@*c-g7+PVXu+b2K2cuRrY+?CWTe+*(OR7dnxD;1`9 z58Epcdchf8d1mTOCq`2m|H+(bK!WlkThV)^xD@{qm&}Vl2fTmCz zLV7+ZQ`EZQY3MS2xv?LbWS&gX$U$(v$&0Jf%q6N(xO23P5BxBz*a{I^7K4huRwQhPifFaX6 zetZAvhL~fr^1j|SIQm<}E@~2Sk;_Qt%-RAhnDSCqIV7?CdsFp1`&F@@u;UIYoabzU ztE&mG!el2_wChesTlXT7cJyOINI#B+7)N+NCnHRtLVtg23tA2+Q<9(7V7tbq#KmiQ z(7v>#OyOJ{i<^~IzTaS=3`6^t<~|BGfUPd!#F1ta?xn1cX0z#my1K1R>_{Jorq!7d zNnN<%Z}9it(IynNglz41szU#QLq*oNoPT-0^Q@53SUBTYjMxp7gCFGTV9ThjQ<`kY zNq%vTzh6;sR-VmmSq~jgIojg3?HLA}zSQo(q+VEUTs}Q}yc0zOKaL$NuEmF@BsSes zMJ#)5{k#re>TX6zThyLqj@>YGQKz;J4?vCcWsuBa8U%fghb+z05W#sbJ>G_j9qREW zHv4-Jm0+1^u)G7brURn`Bh~O_=V(hd%x7_f5B{9>%4|gZVmZemKN+2E3pbx=_QQK$ zy@y)j2o8vImFURPkj?h=*8IQ#=6QpKV`{s>Zlf%#QrL>JP-i-uMMV8Pf+ocN)PH%789xc<^N-`4`|(ukOVNFkaX2@5GiC~Dh@1V$abIN+t5@G} zHRSC^;LKdf!m|cM?Y^5O{j8kDeMYF!%YWH{fUCw~y2-s5ajI;%&^wH5mEk{xgT^3w zTKt{AHv@;>9jh^G8O7OMN2TVz_Jc6bCS6k41#|r*@mGFzxLj}dM&Miti+kl_F~5ph z8+-#-NU|&SAR$=vwd;i;6h5qpdXP%Ty}q0L8R274>iChePH+U(O?LN1_Vq!it?K0N zyUf1iRzLSPs20l;G>Bi73t8Oo>AT8(6U~s?-|P6LtQ#)jcMFu`26092ugfnhXqbGZ zC(zSKM_<@Lqgv1qilo^NgctXqz(Fc|%aabMkUTTQ3#y@55XSSx`#X!v^R&ETxULZz zxewbgD4NCA6fRkuy%YaZd!x2niZkMly(%;qcuNW?ZFM5k}=n?A=nTTK5Lgx zB4eMTt#s2EWD4_rzn3!geP-rJ9k~a~+!$h>mMsX-NpQ&&sb+C&ES^}HC3b;0IQZR{ zGys9V6O|>cG$hzqTy`doLtF6Li_Mc0P}JYD@7vljET`qF8lD_NMNH_%N6fk6V866l zv|P~QSCc%Q`%%_lvO{rpCwLg=BMFmDJd|AFphVBcqrN`PC%QI za7|8w4$mWQUlZF0LHW!5`khPN@bGxqpsUz`AivdPGTLPDjl0$q4;#5a8OE&jm zn|AZg&EH1wTrZA?ev$!eXWAzZrrr|uqMmx+r$I(bAbL`L0FTGN$y+n`gUXLi8e7Hd z5XRx|Bq>qM;x;Dxsx%*O!^^G|Z}Ws6gfHIv;3qVUT66O{^P5aP2>uy*D3O7CZ2Y(8 zEJh*rl1uAjb06d-ePy;Jc4Fo0y`H_hYLT^i*eFb_fW`f`lZ&4)(u@kd%e_WtC=lLM zCBYv?#Xy$-VYN4Od{}c{aobh~x|fq2vjj#E5J!IK=iZBZ;cpdAm3Dw3qp_=5uo}q{ z=j=9Y`aSRDPznNULK;!vm3+IFK}O!G`e$zx25~7VKrNJ$j*cqIXYtH2xLS3b{?axC zWA565^qoCu6n!R8$qu*9BAKnAVBz zewc-8`7F9H`~RZeXlqak~#1GXn#b!fmiL;4QY6sbO))B6vjV`pIw^*DB`Dq+*F^Zz-^53~V#E zHpQKvM9@jc!;ybaA&1a29 zFP;fRSVwoEZ>KV;kLlMmCz0faBXm@7XbBfNPC%VHvT>?!5>COkk9Wk3!F%wAV)ghC zgoL&{y_iLT6Sc}_Sz06BWhb`m+fl*d9^LdkEuT4`PGrOn`;wURl6M#NhdT{Az4^gi zdE*FDEjU)HISG%@(LTW$I|LpUG#SND;=F_yo6?f?H@-;Zl|hVP3Y953bBC!us>4wUXh z53Z02j1}!Ao`@MmYV-&Bqs%>&@9oTmRqc%|JL)?46uDmv;Ar)>?7*xsRJ<6E{^>9c zO7Oh$hO>)cG@fzi;UpaYY%`bab95H(&#{-=JC7s%VL<7_tYIA6FsD5l*9Cz~86V{h z>RH^&DmfCSt$lFPyVBy7PDjd*J$CnXr$F2Nm91|30)mmG%vQdLNcRmAuji+cd0cqh zLYDy_jV?>#XDSLDJ?{~j`{GRc_QjEzS{7G!9qG_^=Kf<9;a@ryOG9L*N2kipNqo3? z`qXXG940n#jPg}4fal<7j0J59_rrQ;*J&~5ikMe-89f6?bzbA|uig%OYG$keGk(AP z|4-yH%PeyWw1hPuPiv2YoownKC@=xq*&Ge)hFMs?N(~O(ynxEni%JX2rqIK?B)NcyW1}F?OEW5xK5p4lEy;kmY+g`^@(lh5Y*^;C literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_500kW/SMN.npy b/openconcept/components/empirical_data/n+3/power_on_500kW/SMN.npy new file mode 100644 index 0000000000000000000000000000000000000000..37b6984bc268b02a8768f374ab3ac01bf3d25873 GIT binary patch literal 5056 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 T!1fO literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_500kW/alt.npy b/openconcept/components/empirical_data/n+3/power_on_500kW/alt.npy new file mode 100644 index 0000000000000000000000000000000000000000..48473b3643abfc2f2ce63c230fa467da82a9a9f6 GIT binary patch literal 5056 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I#iItu1G3WkQ7ItsN4WC1P)5DL6X0aW17Djj6q zfQ0WTJsJ+6bVjRm&~X_OzN7SLIDpa_t_f|)m(&OqfDEWM7VGg!KyHa(ioM$?(G zF{1vZcKU*uH=52y)7fY`!)(`$w$DI4if2@B|HJwdu=0o6^k{iDTAq!TXY?!2Ao-X2 aB(=vgVCKQfA8OOoF2`Wzjh1JS@(ciC&+1eF literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_500kW/t4.npy b/openconcept/components/empirical_data/n+3/power_on_500kW/t4.npy new file mode 100644 index 0000000000000000000000000000000000000000..59b0c2935da3b03259e191c68b2dd64baaeebf61 GIT binary patch literal 5056 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 literal 0 HcmV?d00001 diff --git a/openconcept/components/empirical_data/n+3/power_on_500kW/throttle.npy b/openconcept/components/empirical_data/n+3/power_on_500kW/throttle.npy new file mode 100644 index 0000000000000000000000000000000000000000..919640dbb1a8ef39369599acac724317cce8a932 GIT binary patch literal 5056 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 Date: Fri, 25 Sep 2020 13:49:30 -0400 Subject: [PATCH 03/23] Add ISA temp offset to standard atmosphere --- .../atmospherics/compute_atmos_props.py | 114 ++---------------- .../analysis/atmospherics/density_comp.py | 24 ++-- .../analysis/atmospherics/pressure_comp.py | 14 +-- .../atmospherics/speedofsound_comp.py | 14 +-- .../analysis/atmospherics/temperature_comp.py | 16 +-- .../atmospherics/tests/test_atmospherics.py | 22 ++++ 6 files changed, 64 insertions(+), 140 deletions(-) diff --git a/openconcept/analysis/atmospherics/compute_atmos_props.py b/openconcept/analysis/atmospherics/compute_atmos_props.py index db7dfbe3..e5d9068e 100644 --- a/openconcept/analysis/atmospherics/compute_atmos_props.py +++ b/openconcept/analysis/atmospherics/compute_atmos_props.py @@ -20,6 +20,8 @@ class ComputeAtmosphericProperties(Group): Altitude (vector, km) fltcond|Ueas : float Equivalent airspeed (vector, m/s) + fltcond|TempIncrement : float + Temperature increment for non-standard day (vector, degC) Outputs ------- @@ -50,115 +52,13 @@ def initialize(self): def setup(self): nn = self.options['num_nodes'] tas_in = self.options['true_airspeed_in'] - self.add_subsystem('inputconv', InputConverter(num_nodes=nn),promotes_inputs=['*']) - self.add_subsystem('temp', TemperatureComp(num_nodes=nn)) - self.add_subsystem('pressure',PressureComp(num_nodes=nn)) - self.add_subsystem('density',DensityComp(num_nodes=nn)) - self.add_subsystem('speedofsound',SpeedOfSoundComp(num_nodes=nn)) - self.add_subsystem('outputconv',OutputConverter(num_nodes=nn),promotes_outputs=['*']) + 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=['*']) 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=["*"]) - - self.connect('inputconv.h_km','temp.h_km') - self.connect('inputconv.h_km','pressure.h_km') - self.connect('pressure.p_MPa','density.p_MPa') - self.connect('temp.T_1e2_K',['density.T_1e2_K','speedofsound.T_1e2_K']) - self.connect('pressure.p_MPa','outputconv.p_MPa') - self.connect('temp.T_1e2_K','outputconv.T_1e2_K') - self.connect('speedofsound.a_1e2_ms','outputconv.a_1e2_ms') - self.connect('density.rho_kg_m3','outputconv.rho_kg_m3') - - -class InputConverter(ExplicitComponent): - """ - This component adds a unitized interface to the Hwang and Jasa model. - - Inputs - ------ - fltcond|h : float - Altitude (vector, km) - - Outputs - ------- - h_km : float - Altitude in km to pass to the standard atmosphere modules (vector, unitless) - - 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 flight/control conditions') - def setup(self): - nn = self.options['num_nodes'] - self.add_input('fltcond|h', units='km', desc='Flight condition altitude',shape=(nn,)) - #outputs and partials - self.add_output('h_km', desc='Height in kilometers with no units',shape=(nn,)) - self.declare_partials('h_km','fltcond|h', rows=range(nn), cols=range(nn)) - - def compute(self, inputs, outputs): - outputs['h_km'] = inputs['fltcond|h'] - - def compute_partials(self, inputs, J): - nn = self.options['num_nodes'] - J['h_km','fltcond|h'] = np.ones(nn) - - -class OutputConverter(ExplicitComponent): - """ - This component adds a unitized interface to the Hwang and Jasa model. - - Inputs - ------ - p_MPa : float - Pressure in megapascals from the standard atm model (vector, unitless) - T_1e2_K : float - Tempreature in 100K units from the std atm model (vector, unitless) - rho_kg_m3 : float - Density in kg / m3 from the std atm model (vector, unitless) - - Outputs - ------- - fltcond|p : float - Pressure with units (vector, Pa) - fltcond|rho : float - Density with units (vector, kg/m3) - fltcond|T : float - Temperature with units (vector, K) - - 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 flight/control conditions') - def setup(self): - nn = self.options['num_nodes'] - self.add_input('p_MPa', desc='Flight condition pressures',shape=(nn,)) - self.add_input('T_1e2_K', desc='Flight condition temp',shape=(nn,)) - self.add_input('rho_kg_m3', desc='Flight condition density',shape=(nn,)) - self.add_input('a_1e2_ms', desc='Flight condition speed of sound',shape=(nn,)) - - #outputs and partials - self.add_output('fltcond|p', units='Pa', desc='Flight condition pressure with units',shape=(nn,)) - self.add_output('fltcond|rho', units='kg * m**-3', desc='Flight condition density with units',shape=(nn,)) - self.add_output('fltcond|T', units='K', desc='Flight condition temp with units',shape=(nn,)) - self.add_output('fltcond|a', units='m * s**-1', desc='Flight condition speed of sound with units',shape=(nn,)) - - - self.declare_partials(['fltcond|p'], ['p_MPa'], rows=range(nn), cols=range(nn), val=1e6*np.ones(nn)) - self.declare_partials(['fltcond|rho'], ['rho_kg_m3'], rows=range(nn), cols=range(nn), val=np.ones(nn)) - self.declare_partials(['fltcond|T'], ['T_1e2_K'], rows=range(nn), cols=range(nn), val=100*np.ones(nn)) - self.declare_partials(['fltcond|a'], ['a_1e2_ms'], rows=range(nn), cols=range(nn), val=100*np.ones(nn)) - - def compute(self, inputs, outputs): - outputs['fltcond|p'] = inputs['p_MPa'] * 1e6 - outputs['fltcond|rho'] = inputs['rho_kg_m3'] - outputs['fltcond|T'] = inputs['T_1e2_K'] * 100 - outputs['fltcond|a'] = inputs['a_1e2_ms'] * 100 + self.add_subsystem('machnumber',MachNumberComp(num_nodes=nn),promotes_inputs=["*"],promotes_outputs=["*"]) \ No newline at end of file diff --git a/openconcept/analysis/atmospherics/density_comp.py b/openconcept/analysis/atmospherics/density_comp.py index 17762657..9ce1d3f2 100644 --- a/openconcept/analysis/atmospherics/density_comp.py +++ b/openconcept/analysis/atmospherics/density_comp.py @@ -21,26 +21,26 @@ def initialize(self): def setup(self): num_points = self.options['num_nodes'] - self.add_input('p_MPa', shape=num_points) - self.add_input('T_1e2_K', shape=num_points) - self.add_output('rho_kg_m3', shape=num_points) + 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('rho_kg_m3', 'p_MPa', rows=arange, cols=arange) - self.declare_partials('rho_kg_m3', 'T_1e2_K', 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['p_MPa'] * 1e6 - T_K = inputs['T_1e2_K'] * 1e2 + p_Pa = inputs['fltcond|p'] + T_K = inputs['fltcond|T'] - outputs['rho_kg_m3'] = p_Pa / R / T_K + outputs['fltcond|rho'] = p_Pa / R / T_K def compute_partials(self, inputs, partials): - p_Pa = inputs['p_MPa'] * 1e6 - T_K = inputs['T_1e2_K'] * 1e2 + p_Pa = inputs['fltcond|p'] + T_K = inputs['fltcond|T'] data = 1.0 / R / T_K - partials['rho_kg_m3', 'p_MPa'] = data * 1e6 + partials['fltcond|rho', 'fltcond|p'] = data data = -p_Pa / R / T_K ** 2 - partials['rho_kg_m3', 'T_1e2_K'] = data * 1e2 + partials['fltcond|rho', 'fltcond|T'] = data diff --git a/openconcept/analysis/atmospherics/pressure_comp.py b/openconcept/analysis/atmospherics/pressure_comp.py index 2508e203..cf820320 100644 --- a/openconcept/analysis/atmospherics/pressure_comp.py +++ b/openconcept/analysis/atmospherics/pressure_comp.py @@ -22,26 +22,26 @@ def initialize(self): def setup(self): num_points = self.options['num_nodes'] - self.add_input('h_km', shape=num_points) - self.add_output('p_MPa', shape=num_points, lower=0.) + self.add_input('fltcond|h', shape=num_points, units='m') + self.add_output('fltcond|p', shape=num_points, lower=0., units='Pa') arange = np.arange(num_points) - self.declare_partials('p_MPa', 'h_km', 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'] - h_m = inputs['h_km'] * 1e3 + 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['p_MPa'] = p_Pa / 1e6 + outputs['fltcond|p'] = p_Pa def compute_partials(self, inputs, partials): num_points = self.options['num_nodes'] - h_m = inputs['h_km'] * 1e3 + h_m = inputs['fltcond|h'] derivs = compute_pressure_derivs(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) - partials['p_MPa', 'h_km'] = derivs * 1e3 / 1e6 + partials['fltcond|p', 'fltcond|h'] = derivs diff --git a/openconcept/analysis/atmospherics/speedofsound_comp.py b/openconcept/analysis/atmospherics/speedofsound_comp.py index 3fc70c33..cff53800 100644 --- a/openconcept/analysis/atmospherics/speedofsound_comp.py +++ b/openconcept/analysis/atmospherics/speedofsound_comp.py @@ -24,19 +24,19 @@ def initialize(self): def setup(self): num_points = self.options['num_nodes'] - self.add_input('T_1e2_K', shape=num_points) - self.add_output('a_1e2_ms', shape=num_points) + 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('a_1e2_ms', 'T_1e2_K', rows=arange, cols=arange) + self.declare_partials('fltcond|a', 'fltcond|T', rows=arange, cols=arange) def compute(self, inputs, outputs): - T_K = inputs['T_1e2_K'] * 1e2 + T_K = inputs['fltcond|T'] - outputs['a_1e2_ms'] = np.sqrt(gamma * R * T_K) / 1e2 + outputs['fltcond|a'] = np.sqrt(gamma * R * T_K) def compute_partials(self, inputs, partials): - T_K = inputs['T_1e2_K'] * 1e2 + T_K = inputs['fltcond|T'] data = 0.5 * np.sqrt(gamma * R / T_K) - partials['a_1e2_ms', 'T_1e2_K'] = data + partials['fltcond|a', 'fltcond|T'] = data diff --git a/openconcept/analysis/atmospherics/temperature_comp.py b/openconcept/analysis/atmospherics/temperature_comp.py index 5e260c70..ab02854b 100644 --- a/openconcept/analysis/atmospherics/temperature_comp.py +++ b/openconcept/analysis/atmospherics/temperature_comp.py @@ -22,27 +22,29 @@ def initialize(self): def setup(self): num_points = self.options['num_nodes'] - self.add_input('h_km', shape=num_points) - self.add_output('T_1e2_K', shape=num_points, lower=0.) + 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') arange = np.arange(num_points) - self.declare_partials('T_1e2_K', 'h_km', rows=arange, cols=arange) + 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'] - h_m = inputs['h_km'] * 1e3 + 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['T_1e2_K'] = temp_K / 1e2 + outputs['fltcond|T'] = temp_K + inputs['fltcond|TempIncrement'] def compute_partials(self, inputs, partials): num_points = self.options['num_nodes'] - h_m = inputs['h_km'] * 1e3 + h_m = inputs['fltcond|h'] derivs = compute_temp_derivs(h_m, self.tropos_mask, self.strato_mask, self.smooth_mask) - partials['T_1e2_K', 'h_km'] = derivs * 1e3 / 1e2 + partials['fltcond|T', 'fltcond|h'] = derivs diff --git a/openconcept/analysis/atmospherics/tests/test_atmospherics.py b/openconcept/analysis/atmospherics/tests/test_atmospherics.py index 7d4fe2d3..352188e0 100644 --- a/openconcept/analysis/atmospherics/tests/test_atmospherics.py +++ b/openconcept/analysis/atmospherics/tests/test_atmospherics.py @@ -44,6 +44,28 @@ def test_sea_level_and_30kft(self): 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.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 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) + + def test_partials(self): partials = self.prob.check_partials(method='cs', out_stream=None) assert_check_partials(partials) From 4593a3c40e3ac4b31c1ba960bd65f89ffdb08391 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 14:30:03 -0400 Subject: [PATCH 04/23] Add dymos to requirements --- environment.yml | 3 ++- requirements.txt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index e8070fa6..e570c9ec 100644 --- a/environment.yml +++ b/environment.yml @@ -15,4 +15,5 @@ dependencies: - openmdao - six - sphinx_rtd_theme - - redbaron \ No newline at end of file + - redbaron + - dymos \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 20420757..5d75a770 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ openmdao six sphinx sphinx_rtd_theme -redbaron \ No newline at end of file +redbaron +dymos \ No newline at end of file From 8e411a3d360d0ca0b595fff72c955aa0d287e100 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 14:44:20 -0400 Subject: [PATCH 05/23] Install dymos from git --- environment.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index e570c9ec..1b8a0486 100644 --- a/environment.yml +++ b/environment.yml @@ -16,4 +16,4 @@ dependencies: - six - sphinx_rtd_theme - redbaron - - dymos \ No newline at end of file + - -e git+https://github.com/OpenMDAO/dymos.git@2243f2e8158e31e132782dc72c38ce4e9d47a1ad \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5d75a770..1f198558 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ six sphinx sphinx_rtd_theme redbaron -dymos \ No newline at end of file +-e git+https://github.com/OpenMDAO/dymos.git@2243f2e8158e31e132782dc72c38ce4e9d47a1ad \ No newline at end of file From b003603700035d476fbe2815b0dd1ccaea25e74e Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 14:47:49 -0400 Subject: [PATCH 06/23] continued install problems --- environment.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index 1b8a0486..a6e07e22 100644 --- a/environment.yml +++ b/environment.yml @@ -16,4 +16,4 @@ dependencies: - six - sphinx_rtd_theme - redbaron - - -e git+https://github.com/OpenMDAO/dymos.git@2243f2e8158e31e132782dc72c38ce4e9d47a1ad \ No newline at end of file + - -e git+https://github.com/OpenMDAO/dymos.git@2243f2e8158e31e132782dc72c38ce4e9d47a1ad#egg=dymos \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1f198558..5d91b7d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ six sphinx sphinx_rtd_theme redbaron --e git+https://github.com/OpenMDAO/dymos.git@2243f2e8158e31e132782dc72c38ce4e9d47a1ad \ No newline at end of file +-e git+https://github.com/OpenMDAO/dymos.git@2243f2e8158e31e132782dc72c38ce4e9d47a1ad#egg=dymos \ No newline at end of file From ffafbdb3c24f34f2fb7575f9eb742e0f7353dd3e Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 14:59:32 -0400 Subject: [PATCH 07/23] remove dymos test scratch folder --- scratch/dymos_test.py | 264 ------------------------------------------ 1 file changed, 264 deletions(-) delete mode 100644 scratch/dymos_test.py diff --git a/scratch/dymos_test.py b/scratch/dymos_test.py deleted file mode 100644 index 97cc053c..00000000 --- a/scratch/dymos_test.py +++ /dev/null @@ -1,264 +0,0 @@ -from __future__ import division -from openmdao.api import Group, ExplicitComponent, IndepVarComp, BalanceComp, ImplicitComponent -import openmdao.api as om -import openconcept.api as oc -from openconcept.analysis.atmospherics.compute_atmos_props import ComputeAtmosphericProperties -from openconcept.analysis.aerodynamics import Lift -from openconcept.utilities.math import ElementMultiplyDivideComp, AddSubtractComp -from openconcept.utilities.math.integrals import Integrator -from openconcept.utilities.linearinterp import LinearInterpolator -from openconcept.utilities.math.integrals import Integrator -from openconcept.utilities.dict_indepvarcomp import DymosDesignParamsFromDict -from openconcept.analysis.performance.solver_phases import Groundspeeds, SteadyFlightCL, HorizontalAcceleration -import numpy as np -import copy -import dymos as dm -import matplotlib -import matplotlib.pyplot as plt -from examples.aircraft_data.B738 import data as b738data -from examples.B738 import B738AirplaneModel - -# TODO make a DymosODE group -# Configure method will iterate down and find tags -# Setup_procs method will push DymosODE metadata down -# where to invoke the add_state in the call stack? - -class DymosSteadyFlightODE(om.Group): - """ - Test - """ - def initialize(self): - self.options.declare('num_nodes',default=1) - self.options.declare('aircraft_model') - self.options.declare('flight_phase', default='cruise') - - def setup(self): - nn = self.options['num_nodes'] - 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.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=['*']) - - -def extract_states_from_airplane(acmodel): - pass - -if __name__ == "__main__": - - # - # Define the OpenMDAO problem - # - p = om.Problem(model=om.Group()) - - # - # Define a Trajectory object - # - traj = dm.Trajectory() - p.model.add_subsystem('traj', subsys=traj) - - # - # Define a Dymos Phase object with GaussLobatto Transcription - # - - odekwargs = {'aircraft_model': B738AirplaneModel} - phase0 = dm.Phase(ode_class=DymosSteadyFlightODE, ode_init_kwargs=odekwargs, - transcription=dm.Radau(num_segments=11, order=3, solve_segments=True)) - - traj.add_phase(name='phase0', phase=phase0) - # traj.add_phase(name='phase1', phase=phase1) - acparams = DymosDesignParamsFromDict(b738data, traj) - acparams.add_output_from_dict('ac|aero|polar|e') - acparams.add_output_from_dict('ac|aero|polar|CD0_cruise') - - acparams.add_output_from_dict('ac|geom|wing|S_ref') - acparams.add_output_from_dict('ac|geom|wing|AR') - - acparams.add_output_from_dict('ac|weights|MTOW') - acparams.add_output_from_dict('ac|weights|OEW') - - # - # Set the time options - # Time has no targets in our ODE. - # We fix the initial time so that the it is not a design variable in the optimization. - # The duration of the phase is allowed to be optimized, but is bounded on [0.5, 10]. - # - phase0.set_time_options(fix_initial=True, duration_bounds=(100, 10000), units='s', duration_ref=100., initial_ref0=0.0, initial_ref=1.0) - # phase1.set_time_options(fix_initial=False, duration_bounds=(50, 10000), units='s') - # - # Set the time options - # Initial values of positions and velocity are all fixed. - # The final value of position are fixed, but the final velocity is a free variable. - # The equations of motion are not functions of position, so 'x' and 'y' have no targets. - # The rate source points to the output in the ODE which provides the time derivative of the - # given state. - - - # auto add these - phase0.add_control(name='throttle', units=None, lower=0.0, upper=1.00, targets=['throttle'], ref=1.0) - phase0.add_path_constraint('accel_horiz', lower=0.0, ref=1.) - phase0.add_control(name='fltcond|vs', units='ft/min', lower=400, upper=7000, targets=['fltcond|vs'], opt=False, ref=3000.) - phase0.add_state('fltcond|h', fix_initial=True, fix_final=False, units='km', rate_source='fltcond|vs', targets=['fltcond|h'], ref=10., defect_ref=10.) - phase0.add_boundary_constraint('fltcond|h', loc='final', units='ft', equals=33000., ref=33000.) - phase0.add_control(name='fltcond|Ueas', units='kn', lower=180, upper=250, targets=['fltcond|Ueas'], opt=False, ref=250.) - phase0.add_state('range', fix_initial=True, fix_final=False, units='km', rate_source='fltcond|groundspeed', ref=100., defect_ref=100.) - - # add states for the temperatures - # add states for the battery SOC - # add a control for Tc and Th set - # add a control for duct exit area (with limits) - # add a path constraint Tmotor < 90C > -10C - # add a path constraint Tbattery <70C > 0C - - # custom state - phase0.add_state('fuel_used', fix_initial=True, fix_final=False, units='kg', rate_source='fuel_flow', targets=['fuel_used'], ref=1000., defect_ref=1000.) - # need to know - # rate source location - # target location - # initial condition - # scaler - # defect scaler - # unit - - # phase1.add_control(name='throttle', units=None, lower=0.0, upper=1.5, targets=['throttle']) - # phase1.add_path_constraint('accel_horiz', equals=0.0) - # phase1.add_control(name='fltcond|vs', units='m/s', lower=0, upper=10, targets=['fltcond|vs']) - # phase1.add_state('fltcond|h', fix_initial=True, fix_final=True, units='km', rate_source='fltcond|vs', targets=['fltcond|h']) - # phase1.add_control(name='fltcond|Ueas', units='kn', lower=180, upper=250, targets=['fltcond|Ueas']) - # phase1.add_state('range', fix_initial=False, fix_final=False, units='km', rate_source='fltcond|groundspeed') - # phase1.add_state('fuel_used', fix_initial=False, fix_final=False, units='kg', lower=0.0, rate_source='fuel_flow', targets=['fuel_used']) - - # traj.link_phases(['phase0','phase1']) - # traj.add_design_parameter('ac|weights|MTOW', units='kg', val=500., opt=False, targets={'phase0':['ac|weights|MTOW']}, dynamic=False) - # Minimize final time. - phase0.add_objective('weight', loc='final', ref=-1000.) - # phase0.add_boundary_constraint('time', loc='final', units='s', upper=800.) - - - # Set the driver. - p.driver = om.pyOptSparseDriver() - p.driver.options['optimizer'] = 'SNOPT' - # Allow OpenMDAO to automatically determine our sparsity pattern. - # Doing so can significant speed up the execution of Dymos. - p.driver.declare_coloring() - - # p.model.promotes('traj', inputs=['phase*.rhs_all.ac|*']) - - # Setup the problem - - - p.setup(check=True) - - # Now that the OpenMDAO problem is setup, we can set the values of the states. - p['traj.phase0.t_initial'] = 1.0 - p['traj.phase0.t_duration'] = 900.0 - p.set_val('traj.phase0.states:fltcond|h', - phase0.interpolate(ys=[0, 33000], nodes='state_input'), - units='ft') - - p.set_val('traj.phase0.states:range', - phase0.interpolate(ys=[0, 80], nodes='state_input'), - units='km') - - p.set_val('traj.phase0.states:fuel_used', - phase0.interpolate(ys=[0, 1000], nodes='state_input'), - units='kg') - - p.set_val('traj.phase0.controls:fltcond|Ueas', - phase0.interpolate(ys=[230, 220], nodes='control_input'), - units='kn') - - p.set_val('traj.phase0.controls:fltcond|vs', - phase0.interpolate(ys=[2300, 600], nodes='control_input'), - units='ft/min') - - p.set_val('traj.phase0.controls:throttle', - phase0.interpolate(ys=[0.4, 0.8], nodes='control_input'), - units=None) - # p.set_val('traj.phase1.states:fltcond|h', - # phase1.interpolate(ys=[25000, 27000], nodes='state_input'), - # units='ft') - - # p.set_val('traj.phase1.states:range', - # phase1.interpolate(ys=[50, 60], nodes='state_input'), - # units='km') - - # p.set_val('traj.phase1.states:fuel_used', - # phase1.interpolate(ys=[500, 1000], nodes='state_input'), - # units='kg') - - # p.set_val('traj.phase1.controls:fltcond|Ueas', - # phase1.interpolate(ys=[180, 180], nodes='control_input'), - # units='kn') - - # p.set_val('traj.phase1.controls:fltcond|vs', - # phase1.interpolate(ys=[0, 0], nodes='control_input'), - # units='ft/s') - - # Run the driver to solve the problem - # p['traj.phases.phase0.initial_conditions.initial_value:range'] = 100. - p.run_driver() - - # Check the validity of our results by using scipy.integrate.solve_ivp to - # integrate the solution. - sim_out = traj.simulate() - - # Plot the results - fig, axes = plt.subplots(nrows=1, ncols=4, figsize=(12, 4.5)) - - axes[0].plot(p.get_val('traj.phase0.timeseries.states:range'), - p.get_val('traj.phase0.timeseries.states:fltcond|h'), - 'ro', label='solution') - - axes[0].plot(sim_out.get_val('traj.phase0.timeseries.states:range'), - sim_out.get_val('traj.phase0.timeseries.states:fltcond|h'), - 'b-', label='simulation') - - axes[0].set_xlabel('range (km)') - axes[0].set_ylabel('alt (km)') - axes[0].legend() - axes[0].grid() - - axes[1].plot(p.get_val('traj.phase0.timeseries.time'), - p.get_val('traj.phase0.timeseries.controls:fltcond|Ueas', units='kn'), - 'ro', label='solution') - - axes[1].plot(sim_out.get_val('traj.phase0.timeseries.time'), - sim_out.get_val('traj.phase0.timeseries.controls:fltcond|Ueas', units='kn'), - 'b-', label='simulation') - - axes[1].set_xlabel('time (s)') - axes[1].set_ylabel(r'Command speed (kn)') - axes[1].legend() - axes[1].grid() - - axes[2].plot(p.get_val('traj.phase0.timeseries.time'), - p.get_val('traj.phase0.timeseries.controls:fltcond|vs', units='ft/min'), - 'ro', label='solution') - - axes[2].plot(sim_out.get_val('traj.phase0.timeseries.time'), - sim_out.get_val('traj.phase0.timeseries.controls:fltcond|vs', units='ft/min'), - 'b-', label='simulation') - - axes[2].set_xlabel('time (s)') - axes[2].set_ylabel(r'Command climb rate (ft/min)') - axes[2].legend() - axes[2].grid() - - axes[3].plot(p.get_val('traj.phase0.timeseries.time'), - p.get_val('traj.phase0.timeseries.controls:throttle', units=None), - 'ro', label='solution') - - axes[3].plot(sim_out.get_val('traj.phase0.timeseries.time'), - sim_out.get_val('traj.phase0.timeseries.controls:throttle', units=None), - 'b-', label='simulation') - - axes[3].set_xlabel('time (s)') - axes[3].set_ylabel(r'Throttle') - axes[3].legend() - axes[3].grid() - - plt.show() - # p.check_partials(compact_print=True) - p.model.list_inputs(print_arrays=False, units=True) \ No newline at end of file From 96a5cde2bf1ed043e8fe622c6d0ae5f1347e6225 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 15:04:16 -0400 Subject: [PATCH 08/23] pin openmdao for now --- environment.yml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index a6e07e22..6d08c5fa 100644 --- a/environment.yml +++ b/environment.yml @@ -12,7 +12,7 @@ dependencies: - pip - pip: - pytest - - openmdao + - openmdao<=3.2.1 - six - sphinx_rtd_theme - redbaron diff --git a/requirements.txt b/requirements.txt index 5d91b7d2..faf6e529 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ coverage==4.4 pytest-cov scipy numpy -openmdao +openmdao<=3.2.1 six sphinx sphinx_rtd_theme From dd2d6d2f891318626dc6816ef1e862009546546b Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 15:27:29 -0400 Subject: [PATCH 09/23] identify why 737 test is failing --- openconcept/components/cfm56.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openconcept/components/cfm56.py b/openconcept/components/cfm56.py index 68be2e95..35fb6802 100644 --- a/openconcept/components/cfm56.py +++ b/openconcept/components/cfm56.py @@ -48,8 +48,8 @@ def CFM56(num_nodes=1, plot=False): 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()])) + # 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()])) a = np.array(krigedata) comp = om.MetaModelUnStructuredComp(vec_size=num_nodes) From 9eb451e512b0c3c33cc579290a460f5825bcdf4f Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 16:16:26 -0400 Subject: [PATCH 10/23] Fix trajectory test bugs introduced during dymos work --- .../analysis/tests/test_trajectories.py | 42 +++++++- openconcept/analysis/trajectories.py | 97 ++++++++++--------- 2 files changed, 93 insertions(+), 46 deletions(-) diff --git a/openconcept/analysis/tests/test_trajectories.py b/openconcept/analysis/tests/test_trajectories.py index cef68522..0fca416c 100644 --- a/openconcept/analysis/tests/test_trajectories.py +++ b/openconcept/analysis/tests/test_trajectories.py @@ -376,15 +376,19 @@ def setUp(self): def test_asserts(self): with self.assertRaises(ValueError) as cm: self.p.setup(force_alloc_complex=True) + self.assertEqual('{}'.format(cm.exception), + "Integrator (ic.ode_integ): Variable name 'f_final' already exists.") class TestIntegratorOutsideofPhase(unittest.TestCase): def setUp(self): self.nn = 5 - self.p = om.Problem(model=IntegratorTestDuplicateStateNames(num_nodes=self.nn)) + 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') class TestIntegratorNoIntegratedState(unittest.TestCase): def setUp(self): @@ -400,6 +404,42 @@ def setUp(self): def test_runs(self): self.p.run_model() +class IntegratorGroupWithGroup(IntegratorGroupTestBase): + def setup(self): + super(IntegratorGroupWithGroup, self).setup() + self.add_subsystem('group', om.Group()) + +class TestIntegratorWithGroup(unittest.TestCase): + + class TestPhase(oc.PhaseGroup): + def initialize(self): + 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)) + + def setUp(self): + self.nn = 5 + self.p = om.Problem(model=self.TestPhase(num_nodes=self.nn)) + 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 + 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) + + def test_partials(self): + self.p.run_model() + partials = self.p.check_partials(method='cs', out_stream=None) + assert_check_partials(partials) + + class IntegratorGroupTestPromotedRate(oc.IntegratorGroup): def initialize(self): self.options.declare('num_nodes', default=1) diff --git a/openconcept/analysis/trajectories.py b/openconcept/analysis/trajectories.py index 3f5bf649..d233b447 100644 --- a/openconcept/analysis/trajectories.py +++ b/openconcept/analysis/trajectories.py @@ -75,13 +75,19 @@ def __init__(self, **kwargs): 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'] - self._under_dymos = False - self.add_subsystem('ode_integ', Integrator(time_setup='duration', method='simpson',diff_units=time_units, num_nodes=num_nodes)) + self._under_openconcept = True except KeyError: - # TODO this is a hack. best way would be to check if parent is instance of Dymos phase - self._under_dymos = True + # TODO test_if_under_dymos + if not self._under_dymos: + 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)) + super(IntegratorGroup, self)._setup_procs(pathname, comm, mode, prob_meta) def _configure(self): @@ -89,48 +95,49 @@ def _configure(self): # TODO revisit this when variable data available by default in configure self._setup_var_data() for subsys in self._subsystems_allprocs: - for var in subsys._var_rel_names['output']: - # check if there are any variables to integrate - tags = subsys._var_rel2meta[var]['tags'] - if 'integrate' in tags: - state_name = None - state_units = None - state_val = 0.0 - state_lower = -1e30 - state_upper = 1e30 - state_promotes = False - # TODO Check for duplicates otherwise generic Openmdao duplicate output/input error raised + if not isinstance(subsys, om.Group): + for var in subsys._var_rel_names['output']: + # check if there are any variables to integrate + tags = subsys._var_rel2meta[var]['tags'] + if 'integrate' in tags: + state_name = None + state_units = None + state_val = 0.0 + state_lower = -1e30 + state_upper = 1e30 + state_promotes = False + # 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': - state_name = split_tag[-1] - elif split_tag[0] == 'state_units': - state_units = split_tag[-1] - elif split_tag[0] == 'state_val': - state_val = eval(split_tag[-1]) - elif split_tag[0] == 'state_lower': - state_lower = float(split_tag[-1]) - elif split_tag[0] == 'state_upper': - state_upper = float(split_tag[-1]) - 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) - 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) - # make the rate connection - rate_var_abs_address = subsys.name+'.'+var - if self.pathname: - rate_var_abs_address = self.pathname + '.' + rate_var_abs_address - rate_var_prom_address = self._var_abs2prom['output'][rate_var_abs_address] - self.connect(rate_var_prom_address, 'ode_integ'+'.'+var) - if state_promotes: - self.ode_integ._var_promotes['output'].append(state_name) - self.ode_integ._var_promotes['output'].append(state_name+'_final') - self.ode_integ._var_promotes['input'].append(state_name+'_initial') + for tag in tags: + split_tag = tag.split(':') + if split_tag[0] == 'state_name': + state_name = split_tag[-1] + elif split_tag[0] == 'state_units': + state_units = split_tag[-1] + elif split_tag[0] == 'state_val': + state_val = eval(split_tag[-1]) + elif split_tag[0] == 'state_lower': + state_lower = float(split_tag[-1]) + elif split_tag[0] == 'state_upper': + state_upper = float(split_tag[-1]) + 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) + 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) + # make the rate connection + rate_var_abs_address = subsys.name+'.'+var + if self.pathname: + rate_var_abs_address = self.pathname + '.' + rate_var_abs_address + rate_var_prom_address = self._var_abs2prom['output'][rate_var_abs_address] + self.connect(rate_var_prom_address, 'ode_integ'+'.'+var) + if state_promotes: + self.ode_integ._var_promotes['output'].append(state_name) + self.ode_integ._var_promotes['output'].append(state_name+'_final') + self.ode_integ._var_promotes['input'].append(state_name+'_initial') class TrajectoryGroup(om.Group): def __init__(self, **kwargs): From 9a0c310a5480d9116909ab6ecac5da22e77780dc Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 16:17:16 -0400 Subject: [PATCH 11/23] Add a "BasicMission" with a ground roll phase that's not a real BFL phase --- .../analysis/performance/mission_profiles.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/openconcept/analysis/performance/mission_profiles.py b/openconcept/analysis/performance/mission_profiles.py index 9b7491c8..f84f4c9a 100644 --- a/openconcept/analysis/performance/mission_profiles.py +++ b/openconcept/analysis/performance/mission_profiles.py @@ -126,6 +126,94 @@ def setup(self): self.link_phases(phase5, phase6) self.link_phases(phase6, phase7, states_to_skip=['ode_integ.fltcond|h']) +class BasicMission(oc.TrajectoryGroup): + """ + This analysis group is set up to compute all the major parameters + of a fixed wing mission, including climb, cruise, and descent but no Part 25 reserves + To use this analysis, pass in an aircraft model following OpenConcept interface. + Namely, the model should consume the following: + - flight conditions (fltcond|q/rho/p/T/Utrue/Ueas/...) + - aircraft design parameters (ac|*) + - lift coefficient (fltcond|CL; either solved from steady flight or assumed during ground roll) + - throttle + - propulsor_failed (value 0 when failed, 1 when not failed) + and produce top-level outputs: + - thrust + - drag + - weight + the following parameters need to either be defined as design variables or + given as top-level analysis outputs from the airplane model: + - ac|geom|S_ref + - ac|aero|CL_max_flaps30 + - ac|weights|MTOW + + Inputs + ------ + ac|* : various + All relevant airplane design variables to pass to the airplane model + takeoff|h : float + Takeoff obstacle clearance height (default 50 ft) + cruise|h0 : float + Initial cruise altitude (default 28000 ft) + payload : float + Mission payload (default 1000 lbm) + mission_range : float + Design range (deault 1250 NM) + + Options + ------- + aircraft_model : class + An aircraft model class with the standard OpenConcept interfaces promoted correctly + num_nodes : int + Number of analysis points per segment. Higher is more accurate but more expensive + """ + + def initialize(self): + self.options.declare('num_nodes', default=9, desc="Number of points per segment. 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 segments + 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.fltcond|h_final','climbdt.fltcond|h_final') + self.connect('cruise|h0', 'climb.climbdt.cruise|h0') + self.connect('takeoff|h', 'climb.ode_integ.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.range_final','cruise.cruisedt.range_final') + self.connect('takeoff|h', 'descent.descentdt.takeoff|h') + phase3.connect('ode_integ.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(oc.TrajectoryGroup): """ From 75c0ea7be6c7ec62395ef047df51e6877c959d6b Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 16:17:36 -0400 Subject: [PATCH 12/23] Minor change to CFM56 surrogate --- examples/tests/test_example_aircraft.py | 6 ++++-- openconcept/components/cfm56.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/tests/test_example_aircraft.py b/examples/tests/test_example_aircraft.py index 20ae6570..2af6c870 100644 --- a/examples/tests/test_example_aircraft.py +++ b/examples/tests/test_example_aircraft.py @@ -91,6 +91,8 @@ def setUp(self): def test_values_B738(self): prob = self.prob # block fuel - assert_near_equal(prob.get_val('descent.fuel_used_final', units='lbm'), 28688.32933661591, tolerance=2e-5) + assert_near_equal(prob.get_val('descent.fuel_used_final', units='lbm'), 28549.432517, tolerance=2e-5) + # 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'), 34555.31347454542, tolerance=2e-5) + assert_near_equal(prob.get_val('loiter.fuel_used_final', units='lbm'), 34424.68533072, tolerance=2e-5) + # changelog: 9/2020 - previously 34555.313, updated CFM surrogate model to reject spurious high Mach, low altitude points diff --git a/openconcept/components/cfm56.py b/openconcept/components/cfm56.py index 35fb6802..68be2e95 100644 --- a/openconcept/components/cfm56.py +++ b/openconcept/components/cfm56.py @@ -48,8 +48,8 @@ def CFM56(num_nodes=1, plot=False): 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()])) + 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()])) a = np.array(krigedata) comp = om.MetaModelUnStructuredComp(vec_size=num_nodes) From 1b20951a1102f07b88ed51d623168a1eb6e4f2dc Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 16:17:56 -0400 Subject: [PATCH 13/23] Add tags to the mult-div comp --- openconcept/utilities/math/multiply_divide_comp.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openconcept/utilities/math/multiply_divide_comp.py b/openconcept/utilities/math/multiply_divide_comp.py index 5322bfa7..3ea94ee1 100644 --- a/openconcept/utilities/math/multiply_divide_comp.py +++ b/openconcept/utilities/math/multiply_divide_comp.py @@ -97,7 +97,7 @@ def __init__(self, output_name=None, input_names=None, vec_size=1, length=1, 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): + divide=None, input_units=None, tags=None): """ Add a multiplication relation. @@ -152,10 +152,12 @@ def add_equation(self, output_name, input_names, vec_size=1, length=1, val=1.0, res_ref : float or ndarray Scaling parameter. The value in the user-defined res_units of this output's residual when the scaled value is 1. Default is 1. + 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} + 'res_ref': res_ref, 'tags': tags} self._add_systems.append((output_name, input_names, vec_size, length, val, scaling_factor, divide, input_units, kwargs)) From 6846d632f2a2c547653f436b4693111b4b0d0a16 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 16:21:11 -0400 Subject: [PATCH 14/23] Convergence improvements for the incompressible duct. Add some analytic derivatives and create a full duct model that can take an external HX model as connections --- openconcept/components/ducts.py | 145 ++++++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 16 deletions(-) diff --git a/openconcept/components/ducts.py b/openconcept/components/ducts.py index 6591b07c..ccd41514 100644 --- a/openconcept/components/ducts.py +++ b/openconcept/components/ducts.py @@ -6,6 +6,7 @@ sys.path.insert(0,os.getcwd()) from openconcept.components.heat_exchanger import HXGroup from openconcept.utilities.math.add_subtract_comp import AddSubtractComp +from openconcept.utilities.dvlabel import DVLabel class ExplicitIncompressibleDuct(ExplicitComponent): """ @@ -135,7 +136,7 @@ def setup(self): 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') + self.add_output('T', shape=(nn,), units='K', lower=100.0) self.declare_partials(['*'], ['*'], method='cs') def compute(self, inputs, outputs): @@ -341,13 +342,23 @@ 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') - self.declare_partials(['*'], ['*'], method='cs') + arange = np.arange(nn) + 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'] - outputs['a'] = np.sqrt(gam * R * inputs['T']) + 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 / T) class MachNumberfromSpeed(ExplicitComponent): """ @@ -399,7 +410,7 @@ class HeatAdditionPressureLoss(ExplicitComponent): Mass flow (vector, kg/s) delta_p : float Pressure gain / loss (vector, Pa) - factor_p : float + pressure_recovery : float Total pressure gain / loss as a multiple (vector, dimensionless) heat_in : float Heat addition (subtraction) rate (vector, W) @@ -428,19 +439,40 @@ def setup(self): self.add_input('pt_in', shape=(nn,), units='Pa') self.add_input('mdot', shape=(nn,), units='kg/s') self.add_input('delta_p', shape=(nn,), units='Pa') - self.add_input('factor_p', shape=(nn,), val=np.ones((nn,))) + self.add_input('pressure_recovery', shape=(nn,), val=np.ones((nn,))) self.add_input('heat_in', shape=(nn,), 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') - self.declare_partials(['*'], ['*'], method='cs') + 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'], rows=arange, cols=arange) def compute(self, inputs, outputs): nn = self.options['num_nodes'] + divisor = inputs['cp'] * inputs['mdot'] + mindivisor = np.min(inputs['mdot']) + if mindivisor == 0.0: + raise ValueError(self.msginfo) outputs['Tt_out'] = inputs['Tt_in'] + inputs['heat_in'] / inputs['cp'] / inputs['mdot'] - outputs['pt_out'] = inputs['pt_in'] * inputs['factor_p'] + inputs['delta_p'] + outputs['pt_out'] = inputs['pt_in'] * inputs['pressure_recovery'] + inputs['delta_p'] + + + def compute_partials(self, inputs, J): + 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,)) + class MassFlow(ExplicitComponent): """ @@ -477,7 +509,7 @@ def setup(self): 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') + self.add_output('mdot', shape=(nn,), units='kg/s', lower=1e-8) arange = np.arange(0, nn) self.declare_partials(['mdot'], ['M', 'a', 'rho', 'area'], rows=arange, cols=arange) @@ -501,7 +533,7 @@ def setup(self): 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.3, lower=0.0) + self.add_output('M', shape=(nn,), val=np.ones((nn,))*0.6, lower=0.000001, upper=0.999999) 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) @@ -545,7 +577,7 @@ 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, upper=1.) + self.add_output('nozzle_pressure_ratio', shape=(nn,), val=np.ones((nn,))*0.9, upper=0.99999999) 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) @@ -587,7 +619,7 @@ 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.add_output('M', shape=(nn,), lower=1e-8) self.declare_partials(['*'], ['*'], method='cs') def compute(self, inputs, outputs): @@ -793,7 +825,7 @@ def setup(self): 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') + 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']) @@ -802,7 +834,7 @@ def setup(self): ('area','area_1'), ('delta_p','delta_p_1'), ('heat_in','heat_in_1'), - ('factor_p','pressure_recovery_1')]) + ('pressure_recovery','pressure_recovery_1')]) self.connect('inlet.pt','sta1.pt_in') self.connect('inlet.Tt','sta1.Tt_in') @@ -811,17 +843,17 @@ def setup(self): ('area','area_2'), ('delta_p','delta_p_2'), ('heat_in','heat_in_2'), - ('factor_p','pressure_recovery_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'], + 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', - ('factor_p','pressure_recovery_3'), + ('pressure_recovery','pressure_recovery_3'), ('area','area_3')]) self.connect('sta2.pt_out','sta3.pt_in') self.connect('sta2.Tt_out','sta3.Tt_in') @@ -839,3 +871,84 @@ def setup(self): self.connect('nozzle.p','force.p_nozzle') self.connect('nozzle.rho','force.rho_nozzle') +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' ) + + def setup(self): + 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.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=-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('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') + + # in to HXGroup: + # duct.mdot -> mdot_cold + # mdot_hot + # T_in_hot + # rho_hot + # duct.sta2.T -> T_in_cold + # duct.sta2.rho -> rho_cold + + #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=['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') \ No newline at end of file From e85527cd905cccfab7a046f08454625cc4498d7f Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 16:22:31 -0400 Subject: [PATCH 15/23] Added lower bounds on some components --- openconcept/components/heat_exchanger.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openconcept/components/heat_exchanger.py b/openconcept/components/heat_exchanger.py index 4e7a5719..3a7380be 100644 --- a/openconcept/components/heat_exchanger.py +++ b/openconcept/components/heat_exchanger.py @@ -357,8 +357,8 @@ def setup(self): self.add_input('xs_area_hot', units='m**2') self.add_input('dh_hot', units='m') - self.add_output('Re_dh_cold', shape=(nn,)) - self.add_output('Re_dh_hot', shape=(nn,)) + self.add_output('Re_dh_cold', shape=(nn,), lower=1e-10) + self.add_output('Re_dh_hot', shape=(nn,), lower=1e-10) 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)) @@ -517,8 +517,8 @@ def setup(self): 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') - self.add_output('h_conv_hot', shape=(nn,), units='W/m**2/K') + self.add_output('h_conv_cold', shape=(nn,), units='W/m**2/K', lower=1e-10) + self.add_output('h_conv_hot', shape=(nn,), units='W/m**2/K', lower=1e-10) 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)) @@ -813,7 +813,7 @@ def setup(self): 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,)) + self.add_output('NTU', shape=(nn,), lower=1e-10) self.add_output('heat_max', shape=(nn,), units='W') self.add_output('C_ratio', shape=(nn,)) From 688be77feefd13d4e5d220e23d9317fb9b275f3f Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 17:43:12 -0400 Subject: [PATCH 16/23] various tweaks to duct test group for diagnostics --- openconcept/components/thermal.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openconcept/components/thermal.py b/openconcept/components/thermal.py index 1e7c1b7d..13e2a4ac 100644 --- a/openconcept/components/thermal.py +++ b/openconcept/components/thermal.py @@ -233,11 +233,12 @@ def compute(self, inputs, outputs): 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['rho'] / cross_section_area # m/s + #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 ntu = surface_area * h / Cmin effectiveness = 1 - np.exp(-ntu) @@ -380,7 +381,7 @@ def setup(self): iv = self.add_subsystem('iv',IndepVarComp(), promotes_outputs=['*']) #iv.add_output('q_in', val=10*np.concatenate([np.ones((nn,)),0.5*np.ones((nn,)),0.2*np.ones((nn,))]), units='kW') throttle_profile = np.ones((nn,)) - iv.add_output('q_in',val=10*throttle_profile, units='kW') + iv.add_output('q_in',val=20*throttle_profile, units='kW') #iv.add_output('T_in', val=40*np.ones((nn_tot,)), units='degC') 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') @@ -393,8 +394,8 @@ def setup(self): 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=20) - Ueas = np.ones((nn))*150 - h = np.concatenate([np.linspace(0,25000,nn)]) + Ueas = np.ones((nn))*260 + h = np.concatenate([np.linspace(0,35000,nn)]) iv.add_output('fltcond|Ueas', val=Ueas, units='kn' ) iv.add_output('fltcond|h', val=h, units='ft') @@ -452,7 +453,7 @@ def setup(self): 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=True) - + prob.model.nonlinear_solver.linesearch.options['print_bound_enforce'] = True prob.setup(check=True,force_alloc_complex=True) prob.run_model() @@ -460,7 +461,7 @@ def setup(self): print(np.max(prob['component.T']-273.15)) print(np.max(-prob['duct.force.F_net'])) - prob.check_partials(method='cs', compact_print=True) + # prob.check_partials(method='cs', compact_print=True) #prob.model.list_outputs(units=True, print_arrays=True) if quasi_steady: @@ -496,3 +497,4 @@ def setup(self): plt.xlabel('M_inf') # plt.ylabel('M_nozzle') plt.show() + prob.model.list_outputs(print_arrays=True) \ No newline at end of file From 492a6607e14b52dc46ef05f6927b6e3f7bedfb4f Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 17:47:58 -0400 Subject: [PATCH 17/23] Limit throttle to 1.05 rated --- openconcept/analysis/performance/solver_phases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openconcept/analysis/performance/solver_phases.py b/openconcept/analysis/performance/solver_phases.py index f72ff313..1ef99c84 100644 --- a/openconcept/analysis/performance/solver_phases.py +++ b/openconcept/analysis/performance/solver_phases.py @@ -772,7 +772,7 @@ def setup(self): 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=2.0,units=None,normalize=False,eq_units='m/s**2',rhs_name='accel_horiz',lhs_name='zero_accel',rhs_val=np.zeros((nn,))), + 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 OldSteadyFlightPhase(Group): From f111b84627a4102fd849fa850b5aba320c88bed1 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 17:48:41 -0400 Subject: [PATCH 18/23] Tweaks and doc updates to N3hybrid model --- openconcept/components/N3hybrid.py | 2 +- openconcept/components/N3opt.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openconcept/components/N3hybrid.py b/openconcept/components/N3hybrid.py index fe657fe5..db838bbe 100644 --- a/openconcept/components/N3hybrid.py +++ b/openconcept/components/N3hybrid.py @@ -59,7 +59,7 @@ def N3Opt(num_nodes=1, plot=False): 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('throttle', np.ones((num_nodes,))*1., training_data=a[:,0], units=None, lower=0.0, upper=1.0) 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) diff --git a/openconcept/components/N3opt.py b/openconcept/components/N3opt.py index 93c60e74..b2a374d1 100644 --- a/openconcept/components/N3opt.py +++ b/openconcept/components/N3opt.py @@ -168,6 +168,9 @@ def N3Hybrid(num_nodes=1, plot=False): fltcond|M: float Mach number (vector, dimensionless) + hybrid_power : float + Shaft power added to LP shaft + (vector, kW) Outputs ------- @@ -175,8 +178,8 @@ def N3Hybrid(num_nodes=1, plot=False): Thrust developed by the engine (vector, lbf) fuel_flow : float Fuel flow consumed (vector, lbm/s) - T4 : float - Turbine inlet temperature (vector, Rankine) + surge_margin : float + Surge margin (vector, percent) Options ------- From 5258a0e811bb9312d85b5d56e4c9d7be04372230 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 18:04:18 -0400 Subject: [PATCH 19/23] remove dymos --- environment.yml | 1 - requirements.txt | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index 6d08c5fa..e31c8368 100644 --- a/environment.yml +++ b/environment.yml @@ -16,4 +16,3 @@ dependencies: - six - sphinx_rtd_theme - redbaron - - -e git+https://github.com/OpenMDAO/dymos.git@2243f2e8158e31e132782dc72c38ce4e9d47a1ad#egg=dymos \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index faf6e529..54dba91f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,5 +8,4 @@ openmdao<=3.2.1 six sphinx sphinx_rtd_theme -redbaron --e git+https://github.com/OpenMDAO/dymos.git@2243f2e8158e31e132782dc72c38ce4e9d47a1ad#egg=dymos \ No newline at end of file +redbaron \ No newline at end of file From 011a568e1c0d8c7f4fa3dc1bc7a1be8c255fd442 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Fri, 25 Sep 2020 19:28:42 -0400 Subject: [PATCH 20/23] remove dymos import, reduce tol on B737 test due to differences in scipy model training behavior --- examples/tests/test_example_aircraft.py | 4 ++-- openconcept/analysis/trajectories.py | 1 - requirements.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/tests/test_example_aircraft.py b/examples/tests/test_example_aircraft.py index 47404951..bc8fe2d0 100644 --- a/examples/tests/test_example_aircraft.py +++ b/examples/tests/test_example_aircraft.py @@ -115,8 +115,8 @@ def setUp(self): 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=2e-5) + assert_near_equal(prob.get_val('descent.fuel_used_final', units='lbm'), 28549.432517, tolerance=1e-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=2e-5) + assert_near_equal(prob.get_val('loiter.fuel_used_final', units='lbm'), 34424.68533072, tolerance=1e-4) # changelog: 9/2020 - previously 34555.313, updated CFM surrogate model to reject spurious high Mach, low altitude points diff --git a/openconcept/analysis/trajectories.py b/openconcept/analysis/trajectories.py index d233b447..1ec40b90 100644 --- a/openconcept/analysis/trajectories.py +++ b/openconcept/analysis/trajectories.py @@ -1,7 +1,6 @@ import openmdao.api as om import numpy as np from openconcept.utilities.math.integrals import Integrator -import dymos as dm import warnings # OpenConcept PhaseGroup will be used to hold analysis phases with time integration diff --git a/requirements.txt b/requirements.txt index 54dba91f..ca062170 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ coverage==4.4 pytest-cov scipy numpy -openmdao<=3.2.1 +openmdao==3.2.1 six sphinx sphinx_rtd_theme From 2bf9c7e94487ced92ce08d2ff02b6d8d2f4a2052 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Sat, 26 Sep 2020 13:26:11 -0400 Subject: [PATCH 21/23] Add heat sinks specific to motors and batteries --- openconcept/components/heat_sinks.py | 453 +++++++++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 openconcept/components/heat_sinks.py diff --git a/openconcept/components/heat_sinks.py b/openconcept/components/heat_sinks.py new file mode 100644 index 00000000..975da003 --- /dev/null +++ b/openconcept/components/heat_sinks.py @@ -0,0 +1,453 @@ +import openmdao.api as om +import numpy as np +from openconcept.utilities.math.integrals import Integrator +import warnings + +class BandolierCoolingSystem(om.ExplicitComponent): + """ + Computes battery heat transfer for a parameteric battery + based on Tesla's Model 3 design. + + Assumptions: + - Heat generated uniformly in the cell + - Weight per cell and thermal resistance stay constant + even as specific energy varies parametrically + (this means that cell count is constant with pack WEIGHT, + not pack ENERGY as technology improves) + - Cylindrical cells attached to Tesla-style thermal ribbon + - Liquid cooling + - Heat transfer through axial direction only (not baseplate) + - 2170 cells (21 mm diameter, 70mm tall) + - Battery thermal model assumes unsteady cell temperature, + quasi-steady temperature gradients + + Inputs + ------ + q_in : float + Heat generation rate in the battery (vector, W) + T_in : float + Coolant inlet temperature (vector, K) + T_battery : float + Volume averaged battery temperature (vector, K) + mdot_coolant : float + Mass flow rate of coolant through the bandolier (vector, kg/s) + battery_weight : float + Weight of the battery (overall). Default 100kg (scalar) + n_cpb : float + Number of cells long per "bandolier" actual count is 2x (scalar, default 21, Tesla) + t_channel : float + Thickness (width) of the cooling channel in the bandolier + (scalar, default 1mm) + Outputs + ------- + dTdt : float + Time derivative dT/dt (Tbar in the paper) (vector, K/s) + T_surface : float + Surface temp of the battery (vector, K) + T_core : float + Center temp of the battery (vector, K) + q : float + Heat transfer rate from the motor to the fluid (vector, W) + T_out : float + Outlet fluid temperature (vector, K) + + Options + ------- + num_nodes : float + The number of analysis points to run + coolant_specific_heat : float + Specific heat of the coolant (J/kg/K) (default 3801, glycol/water) + fluid_k : float + Thermal conductivity of the coolant (W/m/K) + nusselt : float + Nusselt number of the coolant channel (default 7.54 for uniform surf temp) + cell_kr : float + Thermal conductivity of the cell in the radial direction (W/m/k) + cell_diameter : float + Battery diameter (default 21mm for 2170 cell) + cell_height : float + Battery height (default 70mm for 2170 cell) + cell_mass : float + Battery weight (default 70g for 2170 cell) + cell_specific_heat : float + Mass average specific heat of the battery (default 900, LiIon cylindrical cell) + 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) + + 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=21.) + 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') + self.add_output('T_core', shape=(nn,), units='K') + self.add_output('q', shape=(nn,), units='W') + self.add_output('T_out', shape=(nn,), units='K') + + 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 + + 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 + + cpf = self.options['coolant_specific_heat'] # of the coolant + + 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 + # 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 + + Ts = (K_cyl * Tbar + Kcell * inputs['T_in']) / (K_cyl + Kcell) + + 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 + + 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'] + + if np.sum(np.abs(qcheck - outputs['q']/n_cells)) > 1e-5: + # 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 + +class LiquidCooledBattery(om.Group): + """A battery with liquid cooling + + Inputs + ------ + q_in : float + Heat produced by the operating component (vector, W) + mdot_coolant : float + Coolant mass flow rate (vector, kg/s) + T_in : float + Instantaneous coolant inflow temperature (vector, K) + battery_weight : float + Battery weight (scalar, kg) + n_cpb : float + Number of cells long per "bandolier" actual count is 2x (scalar, default 21, Tesla) + t_channel : float + Thickness (width) of the cooling channel in the bandolier + (scalar, default 1mm) + T_initial : float + Initial temperature of the battery (only required in thermal mass mode) (scalar, K) + duration : float + Duration of mission segment, only required in unsteady mode + + Outputs + ------- + T_out : float + Instantaneous coolant outlet temperature (vector, K) + T: float + Battery volume averaged temperature (vector, K) + T_core : float + Battery core temperature (vector, K) + T_surface : float + Battery surface temperature (vector, K) + + Options + ------- + num_nodes : int + Number of analysis points to run + quasi_steady : bool + Whether or not to treat the component as having thermal mass + num_nodes : float + The number of analysis points to run + coolant_specific_heat : float + Specific heat of the coolant (J/kg/K) (default 3801, glycol/water) + fluid_k : float + Thermal conductivity of the coolant (W/m/K) + nusselt : float + Nusselt number of the coolant channel (default 7.54 for uniform surf temp) + cell_kr : float + Thermal conductivity of the cell in the radial direction (W/m/k) + cell_diameter : float + Battery diameter (default 21mm for 2170 cell) + cell_height : float + Battery height (default 70mm for 2170 cell) + cell_mass : float + Battery weight (default 70g for 2170 cell) + cell_specific_heat : float + Mass average specific heat of the battery (default 900, LiIon cylindrical cell) + 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('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) + 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']) + + 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=0.0) + 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']) + +class MotorCoolingJacket(om.ExplicitComponent): + """ + Computes motor winding temperature assuming + well-designed, high-power-density aerospace motor. + This component is based on the following assumptions: + - 2020 technology level + - 200kW-1MW class inrunner PM motor + - Liquid cooling of the stators + - "Reasonable" coolant flow rates (component will validate this) + - Thermal performance similiar to the Siemens SP200D motor + + The component assumes a constant heat transfer coefficient based + on the surface area of the motor casing (not counting front and rear faces) + The MagniX Magni 250/500 and Siemens SP200D motors were measured + using rough photogrammetry. + + Magni250: 280kW rated power, ~0.559m OD, 0.2m case "depth" (along thrust axis) + 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, + 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. + 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, + 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 + At full rated power and 95% efficiency, this is 29.5C steady state temp rise + which the right order of magnitude. + + Inputs + ------ + q_in : float + Heat production rate in the motor (vector, W) + T_in : float + Coolant inlet temperature (vector, K) + T : float + Temperature of the motor windings (vector, K) + mdot_coolant : float + Mass flow rate of the coolant (vector, kg/s) + power_rating : float + Rated steady state power of the motor (scalar, W) + + Outputs + ------- + dTdt : float + Time derivative dT/dt (vector, K/s) + q : float + Heat transfer rate from the motor to the fluid (vector, W) + T_out : float + Outlet fluid temperature (vector, K) + + + Options + ------- + num_nodes : float + The number of analysis points to run + coolant_specific_heat : float + Specific heat of the coolant (J/kg/K) (default 3801, glycol/water) + case_cooling_coefficient : float + Watts of heat transfer per square meter of case surface area per K + temperature differential (default 1100 W/m^2/K) + case_area_coefficient : float + rated motor power per square meter of case surface area + (default 650,000 W / m^2) + motor_specific_heat : float + Specific heat of the motor casing (J/kg/K) (default 921, alu) + """ + # TODO reg tests + + 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') + + def setup(self): + 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') + 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'], rows=arange, cols=arange) + self.declare_partials(['T_out'], ['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'] + outputs['q'] = (inputs['T'] - inputs['T_in']) * const * inputs['power_rating'] + outputs['T_out'] = inputs['T_in'] + (inputs['T'] - inputs['T_in']) * const * inputs['power_rating'] / inputs['mdot_coolant'] / self.options['coolant_specific_heat'] + outputs['dTdt'] = (inputs['q_in'] - outputs['q']) / inputs['motor_weight'] / self.options['motor_specific_heat'] + + if np.count_nonzero((inputs['T'] - outputs['T_out']) < 0): + warnings.warn(self.msginfo + ' Motor sink coolant outlet temperature is hotter than the object itself (physically impossible).' + 'This may resolve after the solver converges, but should be double checked.', stacklevel=2) + + def compute_partials(self, inputs, J): + nn = self.options['num_nodes'] + const = self.options['case_cooling_coefficient'] / self.options['case_area_coefficient'] + q = (inputs['T'] - inputs['T_in']) * const * inputs['power_rating'] + J['q', 'T'] = const * inputs['power_rating'] + J['q', 'T_in'] = - const * inputs['power_rating'] + J['q', 'power_rating'] = (inputs['T'] - inputs['T_in']) * const + + J['T_out', 'T'] = const * inputs['power_rating'] / inputs['mdot_coolant'] / self.options['coolant_specific_heat'] + J['T_out', 'T_in'] = np.ones((nn,)) - const * inputs['power_rating'] / inputs['mdot_coolant'] / self.options['coolant_specific_heat'] + J['T_out', 'power_rating'] = (inputs['T'] - inputs['T_in']) * const / inputs['mdot_coolant'] / self.options['coolant_specific_heat'] + J['T_out', 'mdot_coolant'] = - (inputs['T'] - inputs['T_in']) * const * inputs['power_rating'] / inputs['mdot_coolant'] ** 2 / self.options['coolant_specific_heat'] + + J['dTdt', 'q_in'] = 1 / inputs['motor_weight'] / self.options['motor_specific_heat'] + J['dTdt', 'T'] = - const * inputs['power_rating'] / inputs['motor_weight'] / self.options['motor_specific_heat'] + J['dTdt', 'T_in'] = const * inputs['power_rating'] / inputs['motor_weight'] / self.options['motor_specific_heat'] + J['dTdt', 'power_rating'] = - (inputs['T'] - inputs['T_in']) * const / inputs['motor_weight'] / self.options['motor_specific_heat'] + J['dTdt', 'motor_weight'] = - (inputs['q_in'] - q) / inputs['motor_weight'] ** 2 / self.options['motor_specific_heat'] + +class LiquidCooledMotor(om.Group): + """A component (heat producing) with thermal mass + cooled by a cold plate. + + Inputs + ------ + q_in : float + Heat produced by the operating component (vector, W) + mdot_coolant : float + Coolant mass flow rate (vector, kg/s) + T_in : float + Instantaneous coolant inflow temperature (vector, K) + motor_weight : float + Object mass (only required in thermal mass mode) (scalar, kg) + T_initial : float + Initial temperature of the cold plate (only required in thermal mass mode) / object (scalar, K) + duration : float + Duration of mission segment, only required in unsteady mode + power_rating : float + Rated power of the motor (scalar, kW) + + Outputs + ------- + T_out : float + Instantaneous coolant outlet temperature (vector, K) + T: float + Windings temperature (vector, K) + + Options + ------- + motor_specific_heat : float + Specific heat capacity of the object in J / kg / K (default 921 = aluminum) + coolant_specific_heat : float + Specific heat capacity of the coolant in J / kg / K (default 3801, glycol/water) + num_nodes : int + Number of analysis points to run + quasi_steady : bool + Whether or not to treat the component as having thermal mass + """ + + 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') + + 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']), + 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=0.0) + 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']) From f38a64a45c2c531dc613f38490a4c7cab7c97ff5 Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Sat, 26 Sep 2020 13:26:51 -0400 Subject: [PATCH 22/23] Further reduce tol of 738 test --- examples/tests/test_example_aircraft.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/tests/test_example_aircraft.py b/examples/tests/test_example_aircraft.py index bc8fe2d0..796294ea 100644 --- a/examples/tests/test_example_aircraft.py +++ b/examples/tests/test_example_aircraft.py @@ -115,8 +115,8 @@ def setUp(self): 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=1e-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=1e-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 From c28ebe030e472d3fcba803ee7d82d3d97ade668c Mon Sep 17 00:00:00 2001 From: Ben Brelje Date: Sat, 26 Sep 2020 13:32:45 -0400 Subject: [PATCH 23/23] Fix docstring indentation --- openconcept/components/heat_sinks.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openconcept/components/heat_sinks.py b/openconcept/components/heat_sinks.py index 975da003..87f84ddb 100644 --- a/openconcept/components/heat_sinks.py +++ b/openconcept/components/heat_sinks.py @@ -9,17 +9,17 @@ class BandolierCoolingSystem(om.ExplicitComponent): based on Tesla's Model 3 design. Assumptions: - - Heat generated uniformly in the cell - - Weight per cell and thermal resistance stay constant + Heat generated uniformly in the cell + Weight per cell and thermal resistance stay constant even as specific energy varies parametrically (this means that cell count is constant with pack WEIGHT, not pack ENERGY as technology improves) - - Cylindrical cells attached to Tesla-style thermal ribbon - - Liquid cooling - - Heat transfer through axial direction only (not baseplate) - - 2170 cells (21 mm diameter, 70mm tall) - - Battery thermal model assumes unsteady cell temperature, - quasi-steady temperature gradients + Cylindrical cells attached to Tesla-style thermal ribbon + Liquid cooling + Heat transfer through axial direction only (not baseplate) + 2170 cells (21 mm diameter, 70mm tall) + Battery thermal model assumes unsteady cell temperature, + quasi-steady temperature gradients Inputs ------