Skip to content

Commit 5e74b55

Browse files
authored
Merge pull request #97 from crecine/flops_sgm
Initial Implementation of Height Energy in SGM
2 parents 4881260 + d5a24eb commit 5e74b55

16 files changed

+552
-439
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ venv/
109109
ENV/
110110
env.bak/
111111
venv.bak/
112+
.github/*_environment.yml
112113

113114
# Spyder project settings
114115
.spyderproject

aviary/interface/default_phase_info/two_dof_fiti.py

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def create_2dof_based_ascent_phases(
5454
)
5555
accel_vals = {}
5656

57+
# need to set trigger units
5758
climb1_kwargs = dict(
5859
input_speed_type=SpeedType.EAS,
5960
input_speed_units='kn',

aviary/interface/methods_for_level2.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ def load_inputs(self, aviary_inputs, phase_info=None, engine_builder=None):
302302
elif self.mission_method is SOLVED:
303303
from aviary.interface.default_phase_info.solved import phase_info
304304

305-
print('Loaded default phase_info for'
305+
print('Loaded default phase_info for '
306306
f'{self.mission_method.value.lower()} equations of motion')
307307

308308
# create a new dictionary that only contains the phases from phase_info

aviary/mission/flops_based/ode/landing_ode.py

+17
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
from aviary.mission.flops_based.ode.landing_eom import FlareEOM, StallSpeed
1313
from aviary.mission.flops_based.ode.takeoff_ode import TakeoffODE as _TakeoffODE
1414
from aviary.mission.gasp_based.flight_conditions import FlightConditions
15+
from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs
1516
from aviary.utils.aviary_values import AviaryValues
1617
from aviary.utils.functions import set_aviary_initial_values, promote_aircraft_and_mission_vars
1718
from aviary.variable_info.variables import Aircraft, Dynamic, Mission
1819
from aviary.variable_info.variables_in import VariablesIn
20+
from aviary.variable_info.enums import AnalysisScheme
1921

2022

2123
class ExternalSubsystemGroup(om.Group):
@@ -60,14 +62,29 @@ def initialize(self):
6062
'external_subsystems', default=[],
6163
desc='list of external subsystem builder instances to be added to the ODE')
6264

65+
self.options.declare(
66+
"analysis_scheme",
67+
default=AnalysisScheme.COLLOCATION,
68+
types=AnalysisScheme,
69+
desc="The analysis method that will be used to close the trajectory; for example collocation or time integration",
70+
)
71+
6372
def setup(self):
6473
options = self.options
6574

6675
nn = options["num_nodes"]
76+
analysis_scheme = options['analysis_scheme']
6777
aviary_options = options['aviary_options']
6878
subsystem_options = options['subsystem_options']
6979
core_subsystems = options['core_subsystems']
7080

81+
if analysis_scheme is AnalysisScheme.SHOOTING:
82+
SGM_required_inputs = {
83+
't_curr': {'units': 's'},
84+
Dynamic.Mission.DISTANCE: {'units': 'm'},
85+
}
86+
add_SGM_required_inputs(self, SGM_required_inputs)
87+
7188
self.add_subsystem(
7289
'input_port', VariablesIn(aviary_options=aviary_options),
7390
promotes_inputs=['*'], promotes_outputs=['*'])

aviary/mission/flops_based/ode/mission_ODE.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from dymos.models.atmosphere import USatm1976Comp
44

55
from aviary.mission.flops_based.ode.mission_EOM import MissionEOM
6+
from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs, add_SGM_required_outputs
67
from aviary.utils.aviary_values import AviaryValues
78
from aviary.utils.functions import promote_aircraft_and_mission_vars
89
from aviary.variable_info.variable_meta_data import _MetaData
9-
from aviary.variable_info.variables import Dynamic, Mission
10+
from aviary.variable_info.variables import Dynamic, Mission, Aircraft
1011
from aviary.variable_info.variables_in import VariablesIn
12+
from aviary.variable_info.enums import AnalysisScheme
1113

1214

1315
class ExternalSubsystemGroup(om.Group):
@@ -44,15 +46,29 @@ def initialize(self):
4446
values=['path_constraint', 'boundary_constraint', 'bounded', None],
4547
desc='flag to enforce throttle constraints on the path or at the segment boundaries or using solver bounds'
4648
)
49+
self.options.declare(
50+
"analysis_scheme",
51+
default=AnalysisScheme.COLLOCATION,
52+
types=AnalysisScheme,
53+
desc="The analysis method that will be used to close the trajectory; for example collocation or time integration",
54+
)
4755

4856
def setup(self):
4957
options = self.options
5058
nn = options['num_nodes']
59+
analysis_scheme = options['analysis_scheme']
5160
aviary_options = options['aviary_options']
5261
core_subsystems = options['core_subsystems']
5362
subsystem_options = options['subsystem_options']
5463
engine_count = len(aviary_options.get_val('engine_models'))
5564

65+
if analysis_scheme is AnalysisScheme.SHOOTING:
66+
SGM_required_inputs = {
67+
't_curr': {'units': 's'},
68+
Dynamic.Mission.DISTANCE: {'units': 'm'},
69+
}
70+
add_SGM_required_inputs(self, SGM_required_inputs)
71+
5672
self.add_subsystem(
5773
'input_port',
5874
VariablesIn(aviary_options=aviary_options,
@@ -169,9 +185,12 @@ def setup(self):
169185
promotes_inputs=['*'],
170186
promotes_outputs=['*'])
171187

188+
self.set_input_defaults(Dynamic.Mission.MACH, val=np.ones(nn), units='unitless')
172189
self.set_input_defaults(Dynamic.Mission.MASS, val=np.ones(nn), units='kg')
173190
self.set_input_defaults(Dynamic.Mission.VELOCITY, val=np.ones(nn), units='m/s')
174191
self.set_input_defaults(Dynamic.Mission.ALTITUDE, val=np.ones(nn), units='m')
192+
self.set_input_defaults(Dynamic.Mission.ALTITUDE_RATE,
193+
val=np.ones(nn), units='m/s')
175194
self.set_input_defaults(
176195
Dynamic.Mission.THROTTLE, val=np.ones((nn, engine_count)),
177196
units='unitless')
@@ -198,11 +217,19 @@ def setup(self):
198217
],
199218
promotes_outputs=['initial_mass_residual'])
200219

220+
if analysis_scheme is AnalysisScheme.SHOOTING:
221+
SGM_required_outputs = {
222+
Dynamic.Mission.ALTITUDE_RATE: {'units': 'm/s'},
223+
}
224+
add_SGM_required_outputs(self, SGM_required_outputs)
225+
226+
print_level = 0 if analysis_scheme is AnalysisScheme.SHOOTING else 2
227+
201228
self.nonlinear_solver = om.NewtonSolver(solve_subsystems=True,
202229
atol=1.0e-10,
203230
rtol=1.0e-10,
204231
)
205232
self.nonlinear_solver.linesearch = om.BoundsEnforceLS()
206233
self.linear_solver = om.DirectSolver(assemble_jac=True)
207234
self.nonlinear_solver.options['err_on_non_converge'] = True
208-
self.nonlinear_solver.options['iprint'] = 2
235+
self.nonlinear_solver.options['iprint'] = print_level

aviary/mission/flops_based/ode/takeoff_ode.py

+17
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
from aviary.mission.flops_based.ode.takeoff_eom import StallSpeed, TakeoffEOM
99
from aviary.mission.gasp_based.flight_conditions import FlightConditions
10+
from aviary.mission.gasp_based.ode.time_integration_base_classes import add_SGM_required_inputs
1011
from aviary.utils.aviary_values import AviaryValues
1112
from aviary.utils.functions import set_aviary_initial_values, promote_aircraft_and_mission_vars
1213
from aviary.variable_info.variables import Aircraft, Dynamic, Mission
1314
from aviary.variable_info.variables_in import VariablesIn
15+
from aviary.variable_info.enums import AnalysisScheme
1416

1517

1618
class ExternalSubsystemGroup(om.Group):
@@ -58,14 +60,29 @@ def initialize(self):
5860
'climbing', default=False, types=bool,
5961
desc='mode of operation (ground roll or flight)')
6062

63+
self.options.declare(
64+
"analysis_scheme",
65+
default=AnalysisScheme.COLLOCATION,
66+
types=AnalysisScheme,
67+
desc="The analysis method that will be used to close the trajectory; for example collocation or time integration",
68+
)
69+
6170
def setup(self):
6271
options = self.options
6372

6473
nn = options["num_nodes"]
74+
analysis_scheme = options['analysis_scheme']
6575
aviary_options = options['aviary_options']
6676
subsystem_options = options['subsystem_options']
6777
core_subsystems = options['core_subsystems']
6878

79+
if analysis_scheme is AnalysisScheme.SHOOTING:
80+
SGM_required_inputs = {
81+
't_curr': {'units': 's'},
82+
Dynamic.Mission.DISTANCE: {'units': 'm'},
83+
}
84+
add_SGM_required_inputs(self, SGM_required_inputs)
85+
6986
self.add_subsystem(
7087
'input_port', VariablesIn(aviary_options=aviary_options),
7188
promotes_inputs=['*'], promotes_outputs=['*'])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import openmdao.api as om
2+
from openmdao.utils.assert_utils import assert_near_equal
3+
4+
from aviary.interface.methods_for_level2 import AviaryGroup
5+
from aviary.mission.gasp_based.phases.time_integration_traj import FlexibleTraj
6+
from aviary.mission.flops_based.phases.time_integration_phases import \
7+
SGMHeightEnergy, SGMDetailedTakeoff, SGMDetailedLanding
8+
from aviary.subsystems.premission import CorePreMission
9+
from aviary.utils.functions import set_aviary_initial_values
10+
from aviary.variable_info.enums import Verbosity
11+
from aviary.variable_info.variables import Aircraft, Dynamic, Mission
12+
from aviary.variable_info.variables_in import VariablesIn
13+
14+
from aviary.interface.default_phase_info.height_energy import aero, prop, geom
15+
from aviary.subsystems.propulsion.engine_deck import EngineDeck
16+
from aviary.utils.process_input_decks import create_vehicle
17+
from aviary.utils.preprocessors import preprocess_propulsion
18+
from aviary.variable_info.variable_meta_data import _MetaData as BaseMetaData
19+
20+
import warnings
21+
import unittest
22+
import importlib
23+
24+
25+
@unittest.skipUnless(importlib.util.find_spec("pyoptsparse") is not None, "pyoptsparse is not installed")
26+
class HE_SGMDescentTestCase(unittest.TestCase):
27+
def setUp(self):
28+
aviary_inputs, initial_guesses = create_vehicle(
29+
'models/test_aircraft/aircraft_for_bench_FwFm.csv')
30+
aviary_inputs.set_val(Aircraft.Engine.SCALED_SLS_THRUST, val=28690, units="lbf")
31+
aviary_inputs.set_val(Dynamic.Mission.THROTTLE, val=0, units="unitless")
32+
aviary_inputs.set_val(Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT,
33+
val=0.0175, units="unitless")
34+
aviary_inputs.set_val(Mission.Takeoff.BRAKING_FRICTION_COEFFICIENT,
35+
val=0.35, units="unitless")
36+
ode_args = dict(aviary_options=aviary_inputs, core_subsystems=[prop, geom, aero])
37+
preprocess_propulsion(aviary_inputs, [EngineDeck(options=aviary_inputs)])
38+
39+
ode_args['num_nodes'] = 1
40+
ode_args['subsystem_options'] = {'core_aerodynamics': {'method': 'computed'}}
41+
42+
self.ode_args = ode_args
43+
self.aviary_inputs = aviary_inputs
44+
self.tol = 1e-5
45+
46+
def setup_prob(self, phases) -> om.Problem:
47+
prob = om.Problem()
48+
prob.driver = om.pyOptSparseDriver()
49+
prob.driver.options["optimizer"] = 'IPOPT'
50+
prob.driver.opt_settings['tol'] = 1.0E-6
51+
prob.driver.opt_settings['mu_init'] = 1e-5
52+
prob.driver.opt_settings['max_iter'] = 50
53+
prob.driver.opt_settings['print_level'] = 5
54+
55+
aviary_options = self.ode_args['aviary_options']
56+
subsystems = self.ode_args['core_subsystems']
57+
58+
traj = FlexibleTraj(
59+
Phases=phases,
60+
promote_all_auto_ivc=True,
61+
traj_final_state_output=[Dynamic.Mission.MASS,
62+
Dynamic.Mission.DISTANCE,
63+
Dynamic.Mission.ALTITUDE],
64+
traj_initial_state_input=[
65+
Dynamic.Mission.MASS,
66+
Dynamic.Mission.DISTANCE,
67+
Dynamic.Mission.ALTITUDE,
68+
],
69+
)
70+
prob.model = AviaryGroup(aviary_options=aviary_options,
71+
aviary_metadata=BaseMetaData)
72+
prob.model.add_subsystem(
73+
'pre_mission',
74+
CorePreMission(aviary_options=aviary_options,
75+
subsystems=subsystems),
76+
promotes_inputs=['aircraft:*', 'mission:*'],
77+
promotes_outputs=['aircraft:*', 'mission:*']
78+
)
79+
prob.model.add_subsystem('traj', traj,
80+
promotes=['aircraft:*', 'mission:*']
81+
)
82+
83+
prob.model.add_subsystem(
84+
"fuel_obj",
85+
om.ExecComp(
86+
"reg_objective = overall_fuel/10000",
87+
reg_objective={"val": 0.0, "units": "unitless"},
88+
overall_fuel={"units": "lbm"},
89+
),
90+
promotes_inputs=[
91+
("overall_fuel", Mission.Summary.TOTAL_FUEL_MASS),
92+
],
93+
promotes_outputs=[("reg_objective", Mission.Objectives.FUEL)],
94+
)
95+
96+
prob.model.add_objective(Mission.Objectives.FUEL, ref=1e4)
97+
98+
prob.model.add_subsystem(
99+
'input_sink',
100+
VariablesIn(aviary_options=self.aviary_inputs,
101+
meta_data=BaseMetaData),
102+
promotes_inputs=['*'],
103+
promotes_outputs=['*'])
104+
105+
with warnings.catch_warnings():
106+
107+
# Set initial default values for all LEAPS aircraft variables.
108+
set_aviary_initial_values(
109+
prob.model, self.aviary_inputs, meta_data=BaseMetaData)
110+
111+
warnings.simplefilter("ignore", om.PromotionWarning)
112+
113+
prob.setup()
114+
115+
return prob
116+
117+
def run_simulation(self, phases, initial_values: dict):
118+
prob = self.setup_prob(phases)
119+
120+
for key, val in initial_values.items():
121+
prob.set_val(key, **val)
122+
123+
prob.run_model()
124+
125+
distance = prob.get_val('traj.distance_final', units='NM')[0]
126+
mass = prob.get_val('traj.mass_final', units='lbm')[0]
127+
alt = prob.get_val('traj.altitude_final', units='ft')[0]
128+
129+
final_states = {'distance': distance, 'mass': mass, 'altitude': alt}
130+
return final_states
131+
132+
# def test_takeoff(self):
133+
# initial_values_takeoff = {
134+
# "traj.altitude_initial": {'val': 0, 'units': "ft"},
135+
# "traj.mass_initial": {'val': 171000, 'units': "lbm"},
136+
# "traj.distance_initial": {'val': 0, 'units': "NM"},
137+
# "traj.velocity": {'val': .1, 'units': "m/s"},
138+
# }
139+
140+
# ode_args = self.ode_args
141+
# ode_args['friction_key'] = Mission.Takeoff.ROLLING_FRICTION_COEFFICIENT
142+
# brake_release_to_decision = SGMDetailedTakeoff(
143+
# ode_args,
144+
# simupy_args=dict(verbosity=Verbosity.DEBUG,)
145+
# )
146+
# brake_release_to_decision.clear_triggers()
147+
# brake_release_to_decision.add_trigger(Dynamic.Mission.VELOCITY, value=167.85, units='kn')
148+
149+
# phases = {'HE': {
150+
# 'ode': brake_release_to_decision,
151+
# 'vals_to_set': {}
152+
# }}
153+
154+
# final_states = self.run_simulation(phases, initial_values_takeoff)
155+
# # assert_near_equal(final_states['altitude'], 500, self.tol)
156+
# assert_near_equal(final_states['velocity'], 167.85, self.tol)
157+
158+
def test_cruise(self):
159+
initial_values_cruise = {
160+
"traj.altitude_initial": {'val': 35000, 'units': "ft"},
161+
"traj.mass_initial": {'val': 171000, 'units': "lbm"},
162+
"traj.distance_initial": {'val': 0, 'units': "NM"},
163+
"traj.mach": {'val': .8, 'units': "unitless"},
164+
}
165+
166+
SGMCruise = SGMHeightEnergy(
167+
self.ode_args,
168+
phase_name='cruise',
169+
simupy_args=dict(verbosity=Verbosity.QUIET,)
170+
)
171+
SGMCruise.triggers[0].value = 160000
172+
173+
phases = {'HE': {
174+
'ode': SGMCruise,
175+
'vals_to_set': {}
176+
}}
177+
178+
final_states = self.run_simulation(phases, initial_values_cruise)
179+
assert_near_equal(final_states['mass'], 160000, self.tol)
180+
181+
# def test_landing(self):
182+
# initial_values_landing = {
183+
# "traj.altitude_initial": {'val': 35000, 'units': "ft"},
184+
# "traj.mass_initial": {'val': 171000, 'units': "lbm"},
185+
# "traj.distance_initial": {'val': 0, 'units': "NM"},
186+
# "traj.velocity": {'val': 300, 'units': "m/s"},
187+
# }
188+
189+
# ode_args = self.ode_args
190+
# ode_args['friction_key'] = Mission.Takeoff.BRAKING_FRICTION_COEFFICIENT
191+
# phases = {'HE': {
192+
# 'ode': SGMDetailedLanding(
193+
# ode_args,
194+
# simupy_args=dict(verbosity=Verbosity.QUIET,)
195+
# ),
196+
# 'vals_to_set': {}
197+
# }}
198+
199+
# final_states = self.run_simulation(phases, initial_values_landing)
200+
# assert_near_equal(final_states['altitude'], 0, self.tol)
201+
202+
203+
if __name__ == '__main__':
204+
unittest.main()

0 commit comments

Comments
 (0)