Solar UAV Optimization Tutorial
The purpose of this tutorial is to illustrate a different type of problem. We assume you have gone through the first optimization tutorial: Regional Jet Optimization. This tutorial will illustrate a little more complex setup that modifies a mission parameter.
Your objective is simple: get a small UAV to fly from San Francisco to San Diego. In fact you can pose that as an optimization problem, with some constraints that govern how it works. There is no requirement to minimize this or maximize that. Of course you could try to minimize something, but you just want something that works for now. Later iterations can do fancier things.
Next we will go into detail about some of the required files. Analyses.py and Plot_mission.py are straightforward from prior tutorials. So we will not go into those in detail, except to say that we are using a UAV weight model in Analyses.py.
Let’s pose the optimization problem first and then setup the rest. We start with the Nexus first as usual. With this design problem there are things you are uncertain of and want to solve for.
You’re not sure if you really need any solar panels on the airplane, so to start there will be none. The solar ratio is the ratio of wing area to solar area. A value of 1 would indicate that the entire top of the wing is covered with solar panels.
# [ tag , initial, [lb,ub], scaling, units ] problem.inputs = np.array([ [ 'wing_area' , 0.5, ( 0.1, 1.5 ), 0.5, Units.meter ], [ 'aspect_ratio' , 10.0, ( 5.0, 20.0 ), 10.0, Units.less ], [ 'dynamic_pressure', 125.0, ( 1.0, 2000.0 ), 125.0, Units.pascals ], [ 'solar_ratio' , 0.0, ( 0.0, 0.97), 1.0, Units.less ], [ 'kv' , 800.0, ( 10.0, 10000.0 ), 800.0, Units['rpm/volt']], ])
Next come the constraints. The first constraint is that the battery energy can never go negative; the math behind this will be elaborated on later. The next constraint is that the plane must have a battery. Finally there are limits to coefficients of lift and throttle settings.
# [ tag, sense, edge, scaling, units ] problem.constraints = np.array([ [ 'energy_constraint', '=', 0.0, 1.0, Units.less], [ 'battery_mass' , '>', 0.0, 1.0, Units.kg ], [ 'CL' , '>', 0.0, 1.0, Units.less], [ 'Throttle_min' , '>', 0.0, 1.0, Units.less], [ 'Throttle_max' , '>', 0.0, 1.0, Units.less], ])
Notice here that all constraints are greater than zero. This is because SciPy’s SLSQP optimization algorithm assumes this form. To correct for these, the values are adjusted in Procedure.py. Other optimization packages such as PyOpt don’t require this strict form.
Finally, the objective. It’s nothing of course! As long as the constraints are met, the goal of the design is satisfied.
# [ tag, scaling, units ] problem.objective = np.array([ [ 'Nothing', 1. , Units.kg], ])
Next, you will setup the vehicle. This is very similar to the prior Solar UAV tutorial, so we will gloss over this. The one noticeable difference is that a lower fidelity energy network is used. This means that most components operate with prescribed efficiencies. For example:
# Component 5 the Motor motor = SUAVE.Components.Energy.Converters.Motor_Lo_Fid() motor.speed_constant = 900. * Units['rpm/volt'] # RPM/volt is standard motor = size_from_kv(motor) motor.gear_ratio = 1. # Gear ratio, no gearbox motor.gearbox_efficiency = 1. # Gear box efficiency, no gearbox motor.motor_efficiency = 0.8; net.motor = motor
Now for the mission setup. Here we assume it will take 1000 km and the plane will cruise off the coast at 1000 feet in altitude. The distance is a bit longer than the straight line distance, but we’re not going to fly through populated areas. The heading, or body rotation, must be set to account for the changes in latitude and longitude to accurately calculate the solar radiation. We will cruise at a constant altitude and assume it takes no time to climb and descend compared to the cruise time.
segment.state.numerics.number_control_points = 50 segment.dynamic_pressure = 115.0 * Units.pascals segment.start_time = time.strptime("Tue, Jun 21 11:00:00 2020", "%a, %b %d %H:%M:%S %Y",) segment.altitude = 1000.0 * Units.feet segment.distance = 1000.0 * Units.km segment.charge_ratio = 1.0 segment.latitude = 37.4 segment.longitude = -122.15 segment.state.conditions.frames.wind.body_rotations[:,2] = 125.* Units.degrees
Finally we have the procedure setup. In the procedure, we resize the vehicle, calculate weights, finalize the analyses, solve the mission, and post process.
Some notes about sizing. Each wing component (main wing, horizontal tail, and vertical tail) needs the surfaces sized based on its area and aspect ratio. Next the solar panels are sized based on the wing area and solar_ratio. Finally the motor is resized based on correlations for the speed constant of the motor.
def simple_sizing(nexus): # Pull out the vehicle vec = nexus.vehicle_configurations.base # Change the dynamic pressure based on the, add a factor of safety vec.envelope.maximum_dynamic_pressure = nexus.missions.mission.segments.cruise.dynamic_pressure*1.2 # Scale the horizontal and vertical tails based on the main wing area vec.wings.horizontal_stabilizer.areas.reference = 0.15 * vec.reference_area vec.wings.vertical_stabilizer.areas.reference = 0.08 * vec.reference_area # wing spans,areas, and chords for wing in vec.wings: # Unpack AR = wing.aspect_ratio S = wing.areas.reference # Set the spans wing.spans.projected = np.sqrt(AR*S) # Set all of the areas for the surfaces wing.areas.wetted = 2.0 * S wing.areas.exposed = 1.0 * wing.areas.wetted wing.areas.affected = 1.0 * wing.areas.wetted # Set all of the chord lengths chord = wing.areas.reference/wing.spans.projected wing.chords.mean_aerodynamic = chord wing.chords.mean_geometric = chord wing.chords.root = chord wing.chords.tip = chord # Size solar panel area wing_area = vec.reference_area spanel = vec.propulsors.network.solar_panel sratio = spanel.ratio solar_area = wing_area*sratio spanel.area = solar_area spanel.mass_properties.mass = solar_area*(0.60 * Units.kg) # Resize the motor motor = vec.propulsors.network.motor kv = motor.speed_constant motor = size_from_kv(motor, kv) # diff the new data vec.store_diff() return nexus
Here the battery is sized and charged. The battery weight consists of everything that is left over from sizing.
def weights_battery(nexus): # Evaluate weights for all of the configurations config = nexus.analyses.base config.weights.evaluate() vec = nexus.vehicle_configurations.base payload = vec.propulsors.network.payload.mass_properties.mass msolar = vec.propulsors.network.solar_panel.mass_properties.mass MTOW = vec.mass_properties.max_takeoff empty = vec.weight_breakdown.empty mmotor = vec.propulsors.network.motor.mass_properties.mass # Calculate battery mass batmass = MTOW - empty - payload - msolar -mmotor bat = vec.propulsors.network.battery initialize_from_mass(bat,batmass) vec.propulsors.network.battery.mass_properties.mass = batmass # Set Battery Charge maxcharge = nexus.vehicle_configurations.base.propulsors.network.battery.max_energy charge = maxcharge nexus.missions.mission.segments.cruise.battery_energy = charge return nexus
The next we run the mission and post process the results. The post_process function will setup the information of importance for the user. The energy constraint is a way of ensuring that nowhere in the mission the battery energy goes negative. The coefficient of lift is limited to 1.2. The throttle is limited to 0.9, to make sure there is excess throttle to climb. Throttle is also limited from going negative. Finally, the objective, nothing is specified to be zero.
def post_process(nexus): # Unpack mis = nexus.missions.mission.segments.cruise vec = nexus.vehicle_configurations.base res = nexus.results.mission.segments.cruise.conditions # Final Energy maxcharge = vec.propulsors.network.battery.max_energy # Energy constraints, the battery doesn't go to zero anywhere, using a P norm p = 8. energies = res.propulsion.battery_energy[:,0]/np.abs(maxcharge) energies[energies>0] = 0.0 # Exclude the values greater than zero energy_constraint = np.sum((np.abs(energies)**p))**(1/p) # CL max constraint, it is the same throughout the mission CL = res.aerodynamics.lift_coefficient # Pack up summary = nexus.summary summary.CL = 1.2 - CL summary.energy_constraint = energy_constraint summary.throttle_min = res.propulsion.throttle summary.throttle_max = 0.9 - res.propulsion.throttle summary.nothing = 0.0 return nexus
Let’s look at the results:
Optimization terminated successfully. (Exit mode 0) Current function value: 0.0 Iterations: 6 Function evaluations: 43 Gradient evaluations: 6 Design Variable Table: [['wing_area' 0.47033164669264826 (0.1, 1.5) 0.5 1.0] ['aspect_ratio' 9.803406982966878 (5.0, 20.0) 10.0 1.0] ['dynamic_pressure' 120.77967631598601 (1.0, 2000.0) 125.0 1.0] ['solar_ratio' 0.09461785224148354 (0.0, 0.97) 1.0 1.0] ['kv' 833.2503881434465 (10.0, 10000.0) 800.0 0.10471975511965977]] Constraint Table: [['energy_constraint' 4.835975275289128e-08 '=' 0.0 1.0 <Quantity(1.0, 'less')>] ['battery_mass' 2.046103692285638 '>' 0.0 1.0 <Quantity(1.0, 'kilogram')>] ['CL' 0.5131461964430024 '>' 0.0 1.0 <Quantity(1.0, 'less')> ['Throttle_min' 0.321677340768132 '>' 0.0 1.0 <Quantity(1.0, 'less')>] ['Throttle_max' 0.578322659231868 '>' 0.0 1.0 <Quantity(1.0, 'less')>]]
Okay looks like SciPy found a feasible solution without too much time. Now let’s review the plots.
So we can tell now that the battery energy doesn’t go all the way to zero. One thing to note, because we have a solar UAV the voltage is regulated differently and the C-rate doesn't apply in the same way. Let’s look at how the solar flux varies throughout the day and how that affects the draw from the battery.
So maybe those solar panels actually are worth it. They seem to decrease the load on the battery considerably during the daytime.