diff --git a/regression/automatic_regression.py b/regression/automatic_regression.py index 53f1347027..161a6f775b 100644 --- a/regression/automatic_regression.py +++ b/regression/automatic_regression.py @@ -108,7 +108,8 @@ 'scripts/VTOL/test_Multicopter.py', 'scripts/VTOL/test_Tiltwing.py', 'scripts/VTOL/test_Stopped_Rotor.py', - 'scripts/weights/weights.py' + 'scripts/weights/weights.py', + 'scripts/turboelectric_HTS_ducted_fan_network/turboelectric_HTS_ducted_fan_network.py' ] # ---------------------------------------------------------------------- diff --git a/regression/scripts/turboelectric_HTS_ducted_fan_network/turboelectric_HTS_ducted_fan_network.py b/regression/scripts/turboelectric_HTS_ducted_fan_network/turboelectric_HTS_ducted_fan_network.py new file mode 100644 index 0000000000..2adb60cabb --- /dev/null +++ b/regression/scripts/turboelectric_HTS_ducted_fan_network/turboelectric_HTS_ducted_fan_network.py @@ -0,0 +1,377 @@ +### @ingroup Regression-scripts-turboelectric_HTS_ducted_fan_network +#turboelectric_HTS_ducted_fan_network.py +# +# Created: Nov 2021, S. Claridge +# + +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- + +import SUAVE + +import numpy as np + +from SUAVE.Components.Energy.Networks.Turboelectric_HTS_Ducted_Fan import Turboelectric_HTS_Ducted_Fan +from SUAVE.Methods.Propulsion.serial_HTS_turboelectric_sizing import serial_HTS_turboelectric_sizing + +from SUAVE.Attributes.Gases import Air +from SUAVE.Attributes.Solids.Copper import Copper + +from SUAVE.Core import ( +Data, Units, +) +from SUAVE.Methods.Propulsion.ducted_fan_sizing import ducted_fan_sizing + +### @ingroup Regression-scripts-turboelectric_HTS_ducted_fan_network +def main(): + + # call the network function + energy_network() + + return + + +def energy_network(): + + # ------------------------------------------------------------------ + # Evaluation Conditions + # ------------------------------------------------------------------ + + # Conditions + ones_1col = np.ones([1,1]) + alt = 10.0 + + # Setup conditions + planet = SUAVE.Attributes.Planets.Earth() + atmosphere = SUAVE.Analyses.Atmospheric.US_Standard_1976() + atmo_data = atmosphere.compute_values(alt,0,True) + working_fluid = SUAVE.Attributes.Gases.Air() + conditions = SUAVE.Analyses.Mission.Segments.Conditions.Aerodynamics() + + # freestream conditions + conditions.freestream.altitude = ones_1col*alt + conditions.freestream.mach_number = ones_1col*0.8 + conditions.freestream.pressure = ones_1col*atmo_data.pressure + conditions.freestream.temperature = ones_1col*atmo_data.temperature + conditions.freestream.density = ones_1col*atmo_data.density + conditions.freestream.dynamic_viscosity = ones_1col*atmo_data.dynamic_viscosity + conditions.freestream.gravity = ones_1col*planet.compute_gravity(alt) + conditions.freestream.isentropic_expansion_factor = ones_1col*working_fluid.compute_gamma(atmo_data.temperature,atmo_data.pressure) + conditions.freestream.Cp = ones_1col*working_fluid.compute_cp(atmo_data.temperature,atmo_data.pressure) + conditions.freestream.R = ones_1col*working_fluid.gas_specific_constant + conditions.freestream.speed_of_sound = ones_1col*atmo_data.speed_of_sound + conditions.freestream.velocity = conditions.freestream.mach_number*conditions.freestream.speed_of_sound + conditions.velocity = conditions.freestream.mach_number*conditions.freestream.speed_of_sound + conditions.q = 0.5*conditions.freestream.density*conditions.velocity**2 + conditions.g0 = conditions.freestream.gravity + + # propulsion conditions + conditions.propulsion.throttle = ones_1col*1.0 + + # ------------------------------------------------------------------ + # Design/sizing conditions + # ------------------------------------------------------------------ + + # Conditions + ones_1col = np.ones([1,1]) + alt_size = 10000.0 + # Setup conditions + planet = SUAVE.Attributes.Planets.Earth() + atmosphere = SUAVE.Analyses.Atmospheric.US_Standard_1976() + atmo_data = atmosphere.compute_values(alt_size,0,True) + working_fluid = SUAVE.Attributes.Gases.Air() + conditions_sizing = SUAVE.Analyses.Mission.Segments.Conditions.Aerodynamics() + + # freestream conditions + conditions_sizing.freestream.altitude = ones_1col*alt_size + conditions_sizing.freestream.mach_number = ones_1col*0.8 + conditions_sizing.freestream.pressure = ones_1col*atmo_data.pressure + conditions_sizing.freestream.temperature = ones_1col*atmo_data.temperature + conditions_sizing.freestream.density = ones_1col*atmo_data.density + conditions_sizing.freestream.dynamic_viscosity = ones_1col*atmo_data.dynamic_viscosity + conditions_sizing.freestream.gravity = ones_1col*planet.compute_gravity(alt_size) + conditions_sizing.freestream.isentropic_expansion_factor = ones_1col*working_fluid.compute_gamma(atmo_data.temperature,atmo_data.pressure) + conditions_sizing.freestream.Cp = ones_1col*working_fluid.compute_cp(atmo_data.temperature,atmo_data.pressure) + conditions_sizing.freestream.R = ones_1col*working_fluid.gas_specific_constant + conditions_sizing.freestream.speed_of_sound = ones_1col*atmo_data.speed_of_sound + conditions_sizing.freestream.velocity = conditions_sizing.freestream.mach_number*conditions_sizing.freestream.speed_of_sound + conditions_sizing.velocity = conditions_sizing.freestream.mach_number*conditions_sizing.freestream.speed_of_sound + conditions_sizing.q = 0.5*conditions_sizing.freestream.density*conditions_sizing.velocity**2 + conditions_sizing.g0 = conditions_sizing.freestream.gravity + + # propulsion conditions + conditions_sizing.propulsion.throttle = ones_1col*1.0 + + state_sizing = Data() + state_sizing.numerics = Data() + state_sizing.conditions = conditions_sizing + state_off_design = Data() + state_off_design.numerics = Data() + state_off_design.conditions = conditions + + + + # ------------------------------------------------------------------ + # Turboelectric HTS Ducted Fan Network + # ------------------------------------------------------------------ + + # Instantiate the Turboelectric HTS Ducted Fan Network + # This also instantiates the component parts of the efan network, then below each part has its properties modified so they are no longer the default properties as created here at instantiation. + efan = Turboelectric_HTS_Ducted_Fan() + efan.tag = 'turbo_fan' + + # Outline of Turboelectric drivetrain components. These are populated below. + # 1. Propulsor Ducted_fan + # 1.1 Ram + # 1.2 Inlet Nozzle + # 1.3 Fan Nozzle + # 1.4 Fan + # 1.5 Thrust + # 2. Motor + # 3. Powersupply + # 4. ESC + # 5. Rotor + # 6. Lead + # 7. CCS + # 8. Cryocooler + # The components are then sized + + # ------------------------------------------------------------------ + # Component 1 - Ducted Fan + + efan.ducted_fan = SUAVE.Components.Energy.Networks.Ducted_Fan() + efan.ducted_fan.tag = 'ducted_fan' + efan.ducted_fan.number_of_engines = 12. + efan.number_of_engines = efan.ducted_fan.number_of_engines + efan.ducted_fan.engine_length = 1.1 * Units.meter + + + # Positioning variables for the propulsor locations - + xStart = 15.0 + xSpace = 1.0 + yStart = 3.0 + ySpace = 1.8 + efan.ducted_fan.origin = [ [xStart+xSpace*5, -(yStart+ySpace*5), -2.0], + [xStart+xSpace*4, -(yStart+ySpace*4), -2.0], + [xStart+xSpace*3, -(yStart+ySpace*3), -2.0], + [xStart+xSpace*2, -(yStart+ySpace*2), -2.0], + [xStart+xSpace*1, -(yStart+ySpace*1), -2.0], + [xStart+xSpace*0, -(yStart+ySpace*0), -2.0], + [xStart+xSpace*5, (yStart+ySpace*5), -2.0], + [xStart+xSpace*4, (yStart+ySpace*4), -2.0], + [xStart+xSpace*3, (yStart+ySpace*3), -2.0], + [xStart+xSpace*2, (yStart+ySpace*2), -2.0], + [xStart+xSpace*1, (yStart+ySpace*1), -2.0], + [xStart+xSpace*0, (yStart+ySpace*0), -2.0] ] # meters + + # copy the ducted fan details to the turboelectric ducted fan network to enable drag calculations + efan.engine_length = efan.ducted_fan.engine_length + efan.origin = efan.ducted_fan.origin + + + # working fluid + efan.ducted_fan.working_fluid = SUAVE.Attributes.Gases.Air() + + # ------------------------------------------------------------------ + # Component 1.1 - Ram + + # to convert freestream static to stagnation quantities + # instantiate + ram = SUAVE.Components.Energy.Converters.Ram() + ram.tag = 'ram' + + # add to the network + efan.ducted_fan.append(ram) + + # ------------------------------------------------------------------ + # Component 1.2 - Inlet Nozzle + + # instantiate + inlet_nozzle = SUAVE.Components.Energy.Converters.Compression_Nozzle() + inlet_nozzle.tag = 'inlet_nozzle' + + # setup + inlet_nozzle.polytropic_efficiency = 0.98 + inlet_nozzle.pressure_ratio = 0.98 + + # add to network + efan.ducted_fan.append(inlet_nozzle) + + # ------------------------------------------------------------------ + # Component 1.3 - Fan Nozzle + + # instantiate + fan_nozzle = SUAVE.Components.Energy.Converters.Expansion_Nozzle() + fan_nozzle.tag = 'fan_nozzle' + + # setup + fan_nozzle.polytropic_efficiency = 0.95 + fan_nozzle.pressure_ratio = 0.99 + + # add to network + efan.ducted_fan.append(fan_nozzle) + + # ------------------------------------------------------------------ + # Component 1.4 - Fan + + # instantiate + fan = SUAVE.Components.Energy.Converters.Fan() + fan.tag = 'fan' + + # setup + fan.polytropic_efficiency = 0.93 + fan.pressure_ratio = 1.7 + + # add to network + efan.ducted_fan.append(fan) + + # ------------------------------------------------------------------ + # Component 1.5 : thrust + + # To compute the thrust + thrust = SUAVE.Components.Energy.Processes.Thrust() + thrust.tag ='compute_thrust' + + # total design thrust (includes all the propulsors) + thrust.total_design = 2.*24000. * Units.N #Newtons + + # design sizing conditions + altitude = 35000.0*Units.ft + mach_number = 0.78 + isa_deviation = 0. + + # add to network + efan.ducted_fan.thrust = thrust + # ------------------------------------------------------------------ + # Component 2 : HTS motor + + efan.motor = SUAVE.Components.Energy.Converters.Motor_Lo_Fid() + efan.motor.tag = 'motor' + # number_of_motors is not used as the motor count is assumed to match the engine count + + # Set the origin of each motor to match its ducted fan + efan.motor.origin = efan.ducted_fan.origin + efan.motor.gear_ratio = 1.0 + efan.motor.gearbox_efficiency = 1.0 + efan.motor.motor_efficiency = 0.96 + + # ------------------------------------------------------------------ + # Component 3 - Powersupply + + efan.powersupply = SUAVE.Components.Energy.Converters.Turboelectric() + efan.powersupply.tag = 'powersupply' + efan.number_of_powersupplies = 2. + efan.powersupply.propellant = SUAVE.Attributes.Propellants.Jet_A() + efan.powersupply.oxidizer = Air() + efan.powersupply.number_of_engines = 2.0 # number of turboelectric machines, not propulsors + efan.powersupply.efficiency = .37 # Approximate average gross efficiency across the product range. + efan.powersupply.volume = 2.36 *Units.m**3. # 3m long from RB211 datasheet. 1m estimated radius. + efan.powersupply.rated_power = 37400.0 *Units.kW + efan.powersupply.mass_properties.mass = 2500.0 *Units.kg # 2.5 tonnes from Rolls Royce RB211 datasheet 2013. + efan.powersupply.specific_power = efan.powersupply.rated_power/efan.powersupply.mass_properties.mass + efan.powersupply.mass_density = efan.powersupply.mass_properties.mass /efan.powersupply.volume + + # ------------------------------------------------------------------ + # Component 4 - Electronic Speed Controller (ESC) + + efan.esc = SUAVE.Components.Energy.Distributors.HTS_DC_Supply() # Could make this where the ESC is defined as a Siemens SD104 + efan.esc.tag = 'esc' + + efan.esc.efficiency = 0.95 # Siemens SD104 SiC Power Electronicss reported to be this efficient + + # ------------------------------------------------------------------ + # Component 5 - HTS rotor (part of the propulsor motor) + + efan.rotor = SUAVE.Components.Energy.Converters.Motor_HTS_Rotor() + efan.rotor.tag = 'rotor' + + efan.rotor.temperature = 50.0 # [K] + efan.rotor.skin_temp = 300.0 # [K] Temp of rotor outer surface is not ambient + efan.rotor.current = 1000.0 # [A] Most of the cryoload will scale with this number if not using HTS Dynamo + efan.rotor.resistance = 0.0001 # [ohm] 20 x 100 nOhm joints should be possible (2uOhm total) so 1mOhm is an overestimation. + efan.rotor.number_of_engines = efan.ducted_fan.number_of_engines + efan.rotor.length = 0.573 * Units.meter # From paper: DOI:10.2514/6.2019-4517 Would be good to estimate this from power instead. + efan.rotor.diameter = 0.310 * Units.meter # From paper: DOI:10.2514/6.2019-4517 Would be good to estimate this from power instead. + rotor_end_area = np.pi*(efan.rotor.diameter/2.0)**2.0 + rotor_end_circumference = np.pi*efan.rotor.diameter + efan.rotor.surface_area = 2.0 * rotor_end_area + efan.rotor.length*rotor_end_circumference + efan.rotor.R_value = 125.0 # [K.m2/W] 2.0 W/m2 based on experience at Robinson Research + + # ------------------------------------------------------------------ + # Component 6 - Copper Supply Leads of propulsion motor rotors + + efan.lead = SUAVE.Components.Energy.Distributors.Cryogenic_Lead() + efan.lead.tag = 'lead' + copper = Copper() + efan.lead.cold_temp = efan.rotor.temperature # [K] + efan.lead.hot_temp = efan.rotor.skin_temp # [K] + efan.lead.current = efan.rotor.current # [A] + efan.lead.length = 0.3 # [m] + efan.lead.material = copper + efan.leads = efan.ducted_fan.number_of_engines * 2.0 # Each motor has two leads to make a complete circuit + + # ------------------------------------------------------------------ + # Component 7 - Rotor Constant Current Supply (CCS) + + efan.ccs = SUAVE.Components.Energy.Distributors.HTS_DC_Supply() + efan.ccs.tag = 'ccs' + + efan.ccs.efficiency = 0.95 # Siemens SD104 SiC Power Electronics reported to be this efficient + + + # ------------------------------------------------------------------ + # Component 8 - Cryocooler, to cool the HTS Rotor + + efan.cryocooler = SUAVE.Components.Energy.Cooling.Cryocooler() + efan.cryocooler.tag = 'cryocooler' + + efan.cryocooler.cooler_type = 'GM' + efan.cryocooler.min_cryo_temp = efan.rotor.temperature # [K] + efan.cryocooler.ambient_temp = 300.0 # [K] + + # Sizing Conditions. The cryocooler may have greater power requirement at low altitude as the cooling requirement may be static during the flight but the ambient temperature may change. + cryo_temp = 50.0 # [K] + amb_temp = 300.0 # [K] + + # Powertrain Sizing + + + # Size powertrain components + ducted_fan_sizing(efan.ducted_fan,mach_number,altitude) + serial_HTS_turboelectric_sizing(efan,mach_number,altitude, cryo_cold_temp = cryo_temp, cryo_amb_temp = amb_temp) + + print("Design thrust ",efan.ducted_fan.design_thrust) + + print("Sealevel static thrust ",efan.ducted_fan.sealevel_static_thrust) + + results_design = efan(state_sizing) + results_off_design = efan(state_off_design) + F = results_design.thrust_force_vector + mdot = results_design.vehicle_mass_rate + F_off_design = results_off_design.thrust_force_vector + mdot_off_design = results_off_design.vehicle_mass_rate + + + # Test the model + # Specify the expected values + expected = Data() + expected.thrust = 47826.12361690928 + expected.mdot = 0.8051162227457257 + + #error data function + error = Data() + error.thrust_error = (F[0][0] - expected.thrust)/expected.thrust + error.mdot_error = (mdot[0][0]-expected.mdot)/expected.mdot + + print(F[0][0]) + print(mdot[0][0]) + for k,v in list(error.items()): + assert(np.abs(v)<1e-6) + + return + +if __name__ == '__main__': + + main() + diff --git a/trunk/SUAVE/Attributes/Solids/Copper.py b/trunk/SUAVE/Attributes/Solids/Copper.py new file mode 100644 index 0000000000..4813e055af --- /dev/null +++ b/trunk/SUAVE/Attributes/Solids/Copper.py @@ -0,0 +1,92 @@ +## @ingroup Attributes-Solids +# Copper.py +# +# Created: Feb 2020, K. Hamilton + +#------------------------------------------------------------------------------- +# Imports +#------------------------------------------------------------------------------- + +from .Solid import Solid +from SUAVE.Core import Units +from scipy import interpolate +from array import * + +#------------------------------------------------------------------------------- +# RRR=50 OFHC Copper Class +#------------------------------------------------------------------------------- + +## @ingroup Attributes-Solid +class Copper(Solid): + + """ Physical Constants Specific to copper RRR=50 OFHC + + Assumptions: + None + + Source: + "PROPERTIES OF SELECTED MATERIALS AT CRYOGENIC TEMPERATURES" Peter E. Bradley and Ray Radebaugh + "A copper resistance temperature scale" Dauphinee, TM and Preston-Thomas, H + + Inputs: + N/A + + Outputs: + N/A + + Properties Used: + None + """ + + def __defaults__(self): + """Sets material properties at instantiation. + + Assumptions: + None + + Source: + N/A + + Inputs: + N/A + + Outputs: + N/A + + Properties Used: + None + """ + + self.density = 8960.0 # [kg/(m**3)] + self.conductivity_electrical = 58391886.09 # [mhos/m] + self.conductivity_thermal = 392.4 # [W/(m*K)] + + def thermal_conductivity(self, temperature): + # Lookup table arrays. Temperature in K, conductivity in W/(m*K) + temperatures = [4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0, 220.0, 240.0, 260.0, 280.0, 300.0] + conductivities = [3.204, 4.668, 6.223, 7.781, 9.273, 10.64, 11.85, 12.87, 13.68, 14.44, 11.63, 8.636, 6.7, 5.611, 5.003, 4.651, 4.439, 4.218, 4.116, 4.06, 4.026, 4.001, 3.982, 3.965, 3.95, 3.936, 3.924] + + # Function that interpolates the lookup table data + c = interpolate.interp1d(temperatures, conductivities, kind = 'cubic', fill_value='extrapolate') + + # Create output variable + + conductivity = c(temperature) + + return conductivity + + + + # lookup table and interpolator for estimating the electrical conductivity of copper at cryogenic temperatures. + def electrical_conductivity(self, temperature): + # Lookup table. Temperature in K, conductivity in mhos/m + temperatures = [4.2, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 38.0, 40.0, 42.0, 44.0, 46.0, 48.0, 50.0, 52.0, 54.0, 56.0, 58.0, 60.0, 64.0, 68.0, 72.0, 76.0, 80.0, 85.0, 90.0, 95.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, 180.0, 190.0, 200.0, 210.0, 220.0, 230.0, 240.0, 250.0, 260.0, 270.0, 273.16, 280.0, 290.0, 300.0, 310.0, 320.0] + conductivities = [62706513.73, 59649351.1, 58823529.41, 57862233.7, 56756705.27, 55547010.82, 54265718.98, 52838319.53, 51265727.5, 49621943.91, 47879990.68, 46119256.86, 44295117.43, 42462678.59, 40594025.68, 38689780.55, 36839664.93, 34982975.23, 33178363.63, 29768952.17, 26747761.29, 23951445.97, 21412914.59, 19097216.66, 17144994.46, 15418051.98, 13912657.74, 12602171.91, 11436522.41, 10413826.85, 9526550.123, 8071135.431, 6915841.211, 6030094.818, 5295406.175, 4719663.549, 4129948.887, 3661837.678, 3283955.516, 2972855.668, 2494427.41, 2147259.712, 1884972.1, 1680269.452, 1516360.227, 1382622.667, 1270747.745, 1176213.988, 1095370.181, 1025134.636, 963654.7402, 909308.7026, 860944.9011, 817561.3102, 778513.0909, 742977.6209, 710652.6341, 701058.8235, 681102.5197, 653862.9927, 628729.7528, 605483.2867, 583918.8609] + + # Function that interpolates the lookup table data + c = interpolate.interp1d(temperatures, conductivities, kind = 'cubic', fill_value='extrapolate') + + + conductivity = c(temperature) + + return conductivity \ No newline at end of file diff --git a/trunk/SUAVE/Attributes/__init__.py b/trunk/SUAVE/Attributes/__init__.py index 7fcfc6d304..36c9d85b8a 100644 --- a/trunk/SUAVE/Attributes/__init__.py +++ b/trunk/SUAVE/Attributes/__init__.py @@ -8,4 +8,4 @@ from . import Atmospheres from . import Propellants from . import Airports -from . import Solids \ No newline at end of file +from . import Solids diff --git a/trunk/SUAVE/Components/Energy/Converters/Motor_HTS_Rotor.py b/trunk/SUAVE/Components/Energy/Converters/Motor_HTS_Rotor.py new file mode 100644 index 0000000000..aa4f8a481b --- /dev/null +++ b/trunk/SUAVE/Components/Energy/Converters/Motor_HTS_Rotor.py @@ -0,0 +1,111 @@ +## @ingroup Components-Energy-Converters +# Motor_HTS_Rotor.py +# +# Created: Feb 2020, K. Hamilton +# Modified: Nov 2021, S. Claridge + +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- + +# suave imports +import SUAVE + +# package imports +from SUAVE.Components.Energy.Energy_Component import Energy_Component + +# ---------------------------------------------------------------------- +# HTS Rotor Class +# ---------------------------------------------------------------------- +## @ingroup Components-Energy-Converters +class Motor_HTS_Rotor(Energy_Component): + """This represents just the rotor of a HTS motor, i.e. the superconducting components. + This is used to estimate the power and cooling required for the HTS components. + The power used here could be considered the same as that used by the motor as inefficiency, however many publications consider the overall motor efficiency, i.e. including cryocooler and/or motor drive electronics. + + Assumptions: + No ACLoss in the HTS, + HTS is operated within the Ic and Tc limits. + i.e the power used by the coil is only due to solder resistances only, and is not affected by the motor output power or speed. + + Source: + None + """ + def __defaults__(self): + """This sets the default values for the component to function. + + Assumptions: + None + + Source: + N/A + + Inputs: + None + + Outputs: + None + + Properties Used: + None + """ + self.temperature = 0.0 # Temperature inside of the rotor [K] + self.skin_temp = 300.0 # Temperature of the outside of the rotor [K] + self.current = 0.0 # HTS coil current. [A] + self.resistance = 0.0 # Resistance of the HTS oils. [ohm] + self.length = 0.0 # Physical size of rotor exterior [m] + self.diameter = 0.0 # Physical size of rotor exterior [m] + self.surface_area = 0.0 # Surface area of the rotor. [m2] + self.R_value = 125.0 # R_Value of the cryostat wall. [K.m2/W] + self.number_of_engines = 2.0 # Number of rotors on the vehicle + + def power(self, current, ambient_temp): + """ Calculates the electrical power draw from the HTS coils, and the total heating load on the HTS rotor cryostat. + + Assumptions: + No ACLoss in the HTS, + HTS is operated within the Ic and Tc limits. + i.e the power used by the coil is only due to solder resistances only. + + Source: + N/A + + Inputs: + current [A] + ambient_temp [K] + + Outputs: + input_power [W] + cryogenic_load [W] + + Properties Used: + self. + temperature [K] + resistance [ohm] + surface_area [m2] + R_value [K.m2/W] + """ + # unpack + cryo_temp = self.temperature + coil_R = self.resistance + surface_area = self.surface_area + r_value = self.R_value + + # Calculate HTS coil power + # This is both the electrical power required to operate the coil, and the thermal load imparted by the coil into the cryostat. + coil_power = coil_R * current**2 + coil_voltage = current*coil_R + + # Estimate heating from external heat conducting through the cryostat wall. + # Given the non-cryogenic armature coils are usually very close to this external wall it is likely the temperature at the wall is above ambient. + Q = (ambient_temp - cryo_temp)/(r_value/surface_area) + + # Sum the heat loads to give total rotor heat load. + cryo_load = coil_power + Q + + # Store the outputs. + self.outputs.coil_power = coil_power + self.outputs.coil_voltage = coil_voltage + self.outputs.cryo_load = cryo_load + + return coil_power diff --git a/trunk/SUAVE/Components/Energy/Converters/Turboelectric.py b/trunk/SUAVE/Components/Energy/Converters/Turboelectric.py new file mode 100644 index 0000000000..54fd756158 --- /dev/null +++ b/trunk/SUAVE/Components/Energy/Converters/Turboelectric.py @@ -0,0 +1,84 @@ +## @ingroup Components-Energy-Converters +# Turboelectric.py +# +# Created: Nov 2019, K. Hamilton +# Modified: Nov 2021, S. Claridge +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- + +# suave imports +import SUAVE + +# package imports +from SUAVE.Core import Units +from SUAVE.Components.Energy.Energy_Component import Energy_Component +from SUAVE.Methods.Power.Turboelectric.Discharge import zero_fidelity + +# ---------------------------------------------------------------------- +# Turboelectric Class +# ---------------------------------------------------------------------- +## @ingroup Components-Energy-Converters +class Turboelectric(Energy_Component): + """This is a turboelectic component. + + Assumptions: + None + + Source: + None + """ + def __defaults__(self): + """This sets the default values for the component to function. + + Assumptions: + None + + Source: + https://new.siemens.com/global/en/products/energy/power-generation/gas-turbines/sgt-a30-a35-rb.html + + Inputs: + None + + Outputs: + None + + Properties Used: + None + """ + self.propellant = None + self.oxidizer = None + self.number_of_engines = 0.0 # number of turboelectric machines, not propulsors + self.efficiency = .37 # Approximate average gross efficiency across the product range. + self.volume = 0.0 + self.rated_power = 0.0 + self.mass_properties.mass = 0.0 + self.specific_power = 0.0 + self.mass_density = 0.0 + self.discharge_model = zero_fidelity # Simply takes the fuel specific power and applies an efficiency. + + + + def energy_calc(self,conditions,numerics): + """This calls the assigned discharge method. + + Assumptions: + None + + Source: + N/A + + Inputs: + see properties used + + Outputs: + mdot [kg/s] (units may change depending on selected model) + + Properties Used: + self.discharge_model(self, conditions, numerics) + """ + + mdot = self.discharge_model(self, conditions, numerics) + return mdot + + diff --git a/trunk/SUAVE/Components/Energy/Converters/__init__.py b/trunk/SUAVE/Components/Energy/Converters/__init__.py index 582857f162..bd01db668d 100644 --- a/trunk/SUAVE/Components/Energy/Converters/__init__.py +++ b/trunk/SUAVE/Components/Energy/Converters/__init__.py @@ -25,3 +25,5 @@ from .Rotor import Rotor from .Lift_Rotor import Lift_Rotor from .Propeller import Propeller +from .Motor_HTS_Rotor import Motor_HTS_Rotor +from .Turboelectric import Turboelectric diff --git a/trunk/SUAVE/Components/Energy/Cooling/Cryocooler.py b/trunk/SUAVE/Components/Energy/Cooling/Cryocooler.py new file mode 100644 index 0000000000..b0bf39b063 --- /dev/null +++ b/trunk/SUAVE/Components/Energy/Cooling/Cryocooler.py @@ -0,0 +1,132 @@ +## @ingroup Components-Energy-Cooling +# Cryocooler.py +# +# Created: Feb 2020, K.Hamilton +# Modified: Nov 2021, S. Claridge + +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- + +# suave imports +import SUAVE + +# package imports +from SUAVE.Core import Data +from SUAVE.Components.Energy.Energy_Component import Energy_Component +import numpy as np + +# ---------------------------------------------------------------------- +# Cryocooler +# ---------------------------------------------------------------------- +## @ingroup Components-Energy-Cooling-Cryocooler +class Cryocooler(Energy_Component): + + """ + Cryocooler provides cooling power to cryogenic components. + Energy is used by this component to provide the cooling, despite the cooling power provided also being an energy inflow. + """ + def __defaults__(self): + + # Initialise cryocooler properties as null values + self.cooler_type = '' + self.rated_power = 0.0 + self.min_cryo_temp = 0.0 + self.ambient_temp = 300.0 + + def energy_calc(self, cooling_power, cryo_temp, amb_temp): + + """ Calculate the power required by the cryocooler based on the cryocooler type, the required cooling power, and the temperature conditions. + + Assumptions: + Based on mass data for Cryomech cryocoolers as per the datasheets for ground based non-massreduced coolers available via the cryomech website: https://www.cryomech.com/cryocoolers/. + The mass is calculated for the requested power level, the cryocooler should be sized for the maximum power level required as its mass will not change during the flight. + The efficiency scales with required cooling power and temperature only. + The temperature difference and efficiency are taken not to scale with ambient temperature. This should not matter in the narrow range of temperatures in which aircraft operate, i.e. for ambient temperatures between -50 and 50 C. + + Source: + https://www.cryomech.com/cryocoolers/ + + Inputs: + + cooling_power - cooling power required of the cryocooler [watts] + cryo_temp - cryogenic output temperature required [kelvin] + amb_temp - ambient temperature the cooler will reject heat to, defaults to 19C [kelvin] + cooler_type - cryocooler type used. + + Outputs: + + input_power - electrical input power required by the cryocooler [watts] + mass - mass of the cryocooler and supporting components [kilogram] + + Properties Used: + N/A + """ + # Prevent unrealistic temperature changes. + if np.amin(cryo_temp) < 1.: + cryo_temp = np.maximum(cryo_temp, 5.) + print("Warning: Less than zero kelvin not possible, setting cryogenic temperature target to 5K.") + + # Warn if ambient temperature is very low. + if np.amin(amb_temp) < 200.: + print("Warning: Suprisingly low ambient temperature, check altitude.") + + # Calculate the shift in achievable minimum temperature based on the the ambient temperature (temp_amb) and the datasheet operating temperature (19C, 292.15K) + temp_offset = 292.15 - amb_temp + + # Calculate the required temperature difference the cryocooler must produce. + temp_diff = amb_temp-cryo_temp + + # Disable if the target temperature is greater than the ambient temp. Technically cooling like this is possible, however there are better cooling technologies to use if this is the required scenario. + if np.amin(temp_diff) < 0.: + temp_diff = np.maximum(temp_diff, 0.) + print("Warning: Temperature conditions are not well suited to cryocooler use. Cryocooler disabled.") + + # Set the parameters of the cooler based on the cooler type and the operating conditions. The default ambient operating temperature (19C) is used as a base. + if self.cooler_type == 'fps': #Free Piston Stirling + temp_minRT = 35.0 # Minimum temperature achievable by this type of cooler when rejecting to an ambient temperature of 19C (K) + temp_min = temp_minRT - temp_offset # Updated minimum achievable temperature based on the supplied ambient temperature (K) + eff = 0.0014*(cryo_temp-temp_min) # Efficiency function. This is a line fit from a survey of Cryomech coolers in November 2019 + input_power = cooling_power/eff # Electrical input power (W) + mass = 0.0098*input_power+1.0769 # Total cooler mass function. Fit from November 2019 Cryomech data. (kg) + + elif self.cooler_type == 'GM': #Gifford McMahon + temp_minRT = 5.4 + temp_min = temp_minRT - temp_offset + eff = 0.0005*(cryo_temp-temp_min) + input_power = cooling_power/eff + mass = 0.0129*input_power+63.08 + + elif self.cooler_type == 'sPT': #Single Pulsetube + temp_minRT = 16.0 + temp_min = temp_minRT - temp_offset + eff = 0.0002*(cryo_temp-temp_min) + input_power = cooling_power/eff + mass = 0.0079*input_power+51.124 + + elif self.cooler_type == 'dPT': #Double Pulsetube + temp_minRT = 8.0 + temp_min = temp_minRT - temp_offset + eff = 0.00001*(cryo_temp-temp_min) + input_power = cooling_power/eff + mass = 0.0111*input_power+73.809 + + else: + print("Warning: Unknown Cryocooler type") + return[0.0,0.0] + + # Warn if the cryogenic temperature is unachievable + diff = cryo_temp - temp_min + if np.amin(diff) < 0.0: + eff = 0.0 + input_power = None + mass = None + print("Warning: The required cryogenic temperature of " + str(cryo_temp) + " is not achievable using a " + self.cooler_type + " cryocooler at an ambient temperature of " + str(amb_temp) + ". The minimum temperature achievable is " + str(temp_min)) + + self.mass_properties.mass = mass + self.rated_power = input_power + + return [input_power, mass] + + + diff --git a/trunk/SUAVE/Components/Energy/Cooling/__init__.py b/trunk/SUAVE/Components/Energy/Cooling/__init__.py new file mode 100644 index 0000000000..304f42f00d --- /dev/null +++ b/trunk/SUAVE/Components/Energy/Cooling/__init__.py @@ -0,0 +1,9 @@ +## @defgroup Components-Energy-Cooling Cooling +# Components that cryogenically cool others +# @ingroup Components-Energy + +# __init__.py +# +# Created: Feb 2020, K.Hamilton + +from .Cryocooler import Cryocooler diff --git a/trunk/SUAVE/Components/Energy/Distributors/Cryogenic_Lead.py b/trunk/SUAVE/Components/Energy/Distributors/Cryogenic_Lead.py new file mode 100644 index 0000000000..d3a591160b --- /dev/null +++ b/trunk/SUAVE/Components/Energy/Distributors/Cryogenic_Lead.py @@ -0,0 +1,219 @@ +## @ingroup Components-Energy-Distributors +# Cryogenic_Lead.py +# +# Created: Feb 2020, K.Hamilton +# Modified: Nov 2021, S. Claridge + +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- + +# suave imports +import SUAVE + +from SUAVE.Components.Energy.Energy_Component import Energy_Component +from SUAVE.Attributes.Solids.Solid import Solid +from scipy import integrate +from scipy import interpolate +from scipy.misc import derivative +import numpy as np +# ---------------------------------------------------------------------- +# Cryogenic Lead Class +# ---------------------------------------------------------------------- + +## @ingroup Components-Energy-Distributors +class Cryogenic_Lead(Energy_Component): + + def __defaults__(self): + """ This sets the default values. + + Assumptions: + Cryogenic Leads only operate at their optimum current, or at zero current. + + Source: + Current Lead Optimization for Cryogenic Operation at Intermediate Temperatures - Broomberg + + Inputs: + None + + Outputs: + None + + Properties Used: + None + """ + self.cold_temp = 0.0 # [K] + self.hot_temp = 0.0 # [K] + self.current = 0.0 # [A] + self.length = 0.0 # [m] + self.material = None + + self.cross_section = 0.0 # [m2] + self.optimum_current = 0.0 # [A] + self.minimum_Q = 0.0 # [W] + self.unpowered_Q = 0.0 # [W] + + def Q_min(self, material, cold_temp, hot_temp, current): + # Estimate the area under the thermal:electrical conductivity vs temperature plot for the temperature range of the current lead. + integral = integrate.quad(lambda T: material.thermal_conductivity(T)/material.electrical_conductivity(T), cold_temp, hot_temp) + + # Estimate the average thermal:electrical conductivity for the lead. + average_ratio = (1/(hot_temp-cold_temp)) * integral[0] + + # Solve the heat flux at the cold end. This is both the load on the cryocooler and the power loss in the current lead. + minimum_Q = current * (2*average_ratio*(hot_temp-cold_temp))**0.5 + # This represents the special case where all the electrical power is delivered to the cryogenic environment as this optimised the lead for reduced cryogenic load. Q = electrical power + power = minimum_Q + + return [minimum_Q, power] + + def LARatio(self, material, cold_temp, hot_temp, current, minimum_Q): + # Calculate the optimum length to cross-sectional area ratio + # Taken directly from McFee + + sigTL = material.electrical_conductivity(cold_temp) + inte = integrate.quad(lambda T: self.Q_min(material,T,hot_temp,current)[0]*derivative(material.electrical_conductivity,T), cold_temp, hot_temp)[0] + la_ratio = (sigTL * minimum_Q + inte)/(current**2) + + return la_ratio + + def initialize_material_lead(self): + """ + Defines an optimum material lead for supplying current to a cryogenic environment given the operating conditions and material properties. + + Assumptions: + None + + Inputs: + lead. + cold_temp [K] + hot_temp [K] + current [A] + length [m] + + Outputs: + lead. + mass [kg] + cross_section [m] + optimum_current [A] + minimum_Q [W] + """ + + # Unpack properties + cold_temp = self.cold_temp + hot_temp = self.hot_temp + current = self.current + length = self.length + material = self.material + + + # Find the heat generated by the optimum lead + minimum_Q = self.Q_min(material, cold_temp, hot_temp, current)[0] + + # # Calculate the optimum length to cross-sectional area ratio + la_ratio = self.LARatio(material, cold_temp, hot_temp, current, minimum_Q) + + # Calculate the cross-sectional area + cs_area = length/la_ratio + # Apply the material density to calculate the mass + mass = cs_area*length*material.density + + # Pack up results + self.mass_properties.mass = mass + self.cross_section = cs_area + self.optimum_current = current + self.minimum_Q = minimum_Q + + # find the heat conducted into the cryogenic environment if no current is flowing + unpowered_Q = self.Q_unpowered() + + # Pack up unpowered lead + self.unpowered_Q = unpowered_Q[0] + + def Q_unpowered(self): + # Estimates the heat flow into the cryogenic environment if no current is supplied to the lead. + + # unpack properties + hot_temp = self.hot_temp + cold_temp = self.cold_temp + cross_section = self.cross_section + length = self.length + material = self.material + + # Integrate the thermal conductivity across the relevant temperature range. + integral = integrate.quad(lambda T: material.thermal_conductivity(T), cold_temp, hot_temp) + + # Apply the conductivity to estimate the heat flow + Q = integral[0] * cross_section / length + + # Electrical power is obviously zero if no current is flowing + power = 0.0 + + return [Q, power] + + def Q_offdesign(self, current): + # Estimates the heat flow into the cryogenic environment when a current other than the current the lead was optimised for is flowing. Assumes the temperature difference remains constant. + values = list(map(self.calc_current, current.tolist())) + values = np.asarray(values) + + return values + + def calc_current(self, current ): + + design_current = self.optimum_current + design_Q = self.minimum_Q + zero_Q = self.unpowered_Q + cold_temp = self.cold_temp + hot_temp = self.hot_temp + cs_area = self.cross_section + length = self.length + material = self.material + + # The thermal gradient along the lead is assumed to remain constant for all currents below the design current. The resistance remains constant if the temperature remains constant. The estimated heat flow is reduced in proportion with the carried current. + if current <= design_current: + proportion = current/design_current + R = design_Q/(design_current**2.0) + power = R*current**2.0 + Q = zero_Q + proportion * (design_Q - zero_Q) + + # If the supplied current is higher than the design current the maximum temperature in the lead will be higher than ambient. Solve by dividing the lead at the maximum temperature point. + else: + # Initial guess at max temp in lead + max_temp = 2 * hot_temp + # Find actual maximum temperature by bisection, accept result within 1% of correct. + error = 1 + guess_over = 0 + guess_diff = hot_temp + + while error > 0.01: + # Find length of warmer part of lead + warm_Q = self.Q_min(material, hot_temp, max_temp, current) + + warm_la = self.LARatio(material, hot_temp, max_temp, current, warm_Q) + warm_length = cs_area * warm_la + # Find length of cooler part of lead + cool_Q = self.Q_min(material, cold_temp, max_temp, current) + cool_la = self.LARatio(material, cold_temp, max_temp, current, cool_Q) + cool_length = cs_area * cool_la + # compare lead length with known lead length as test of the max temp guess + test_length = warm_length + cool_length + error = abs((test_length-length)/length) + # change the guessed max_temp + # A max_temp too low will result in the test length being too long + if test_length > length: + if guess_over == 0: # query whether solving by bisection yet + guess_diff = max_temp # if not, continue to double guess + max_temp = 2*max_temp + else: + max_temp = max_temp + guess_diff + else: + guess_over = 1 # set flag that bisection range found + max_temp = max_temp - guess_diff + # Prepare guess difference for next iteration + guess_diff = 0.5*guess_diff + # The cool_Q is the cryogenic heat load as warm_Q is sunk to ambient + Q = cool_Q + # All Q is out of the lead, so the electrical power use in the lead is the sum of the Qs + power = warm_Q + cool_Q + + return [Q,power] \ No newline at end of file diff --git a/trunk/SUAVE/Components/Energy/Distributors/HTS_DC_Supply.py b/trunk/SUAVE/Components/Energy/Distributors/HTS_DC_Supply.py new file mode 100644 index 0000000000..64ff4ab31e --- /dev/null +++ b/trunk/SUAVE/Components/Energy/Distributors/HTS_DC_Supply.py @@ -0,0 +1,74 @@ +## @ingroup Components-Energy-Distributors +# HTS_DC_Supply.py +# +# Created: Feb 2020, K. Hamilton +# Modified: Nov 2021, S. Claridge + +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- + +# suave imports +import SUAVE + +from SUAVE.Components.Energy.Energy_Component import Energy_Component + +# ---------------------------------------------------------------------- +# HTS DC Supply Class +# ---------------------------------------------------------------------- + +## @ingroup Components-Energy-Distributors +class HTS_DC_Supply(Energy_Component): + + def __defaults__(self): + """ This sets the default values. + + Assumptions: + None + + Source: + N/A + + Inputs: + None + + Outputs: + None + + Properties Used: + None + """ + + self.efficiency = 0.0 + self.rated_current = 100.0 # [A] + self.rated_power = 100.0 # [W] + + def power(self, current, power_out): + """ The power that must be supplied to the DC supply to power the HTS coils. + + Assumptions: + Supply cable is solid copper, i.e. not a rotating joint. + Power supply has static efficiency across current output range. + Power supply performance is not affected by altitude or other environmental factors. This is not generally true (Ametek SGe datasheet should be derated by 10% per 1000 feet) for current supplies designed for ground use however a supply specifically designed for airborne use can be expected to have a more appropriate cooling design that would allow high altitude use. + + Source: + N/A + + Inputs: + current [A] + power_out [W] + self.efficiency + + Outputs: + power_in [W] + + """ + # Unpack + efficiency = self.efficiency + + # Apply the efficiency of the current supply to get the total power required at the input of the current supply. + power_in = power_out/efficiency + + + # Return basic result. + return power_in diff --git a/trunk/SUAVE/Components/Energy/Distributors/__init__.py b/trunk/SUAVE/Components/Energy/Distributors/__init__.py index 80e6a0ef4a..8f579b8c01 100644 --- a/trunk/SUAVE/Components/Energy/Distributors/__init__.py +++ b/trunk/SUAVE/Components/Energy/Distributors/__init__.py @@ -7,5 +7,10 @@ # Created: Jun 2014, E. Botero # Modified: Jan 2016, T. MacDonald + + from .Solar_Logic import Solar_Logic -from .Electronic_Speed_Controller import Electronic_Speed_Controller \ No newline at end of file +from .Electronic_Speed_Controller import Electronic_Speed_Controller +from .Cryogenic_Lead import Cryogenic_Lead +from .HTS_DC_Supply import HTS_DC_Supply + diff --git a/trunk/SUAVE/Components/Energy/Networks/Turboelectric_HTS_Ducted_Fan.py b/trunk/SUAVE/Components/Energy/Networks/Turboelectric_HTS_Ducted_Fan.py new file mode 100644 index 0000000000..7af3123923 --- /dev/null +++ b/trunk/SUAVE/Components/Energy/Networks/Turboelectric_HTS_Ducted_Fan.py @@ -0,0 +1,179 @@ +## @ingroup Components-Energy-Networks +# Turboelectric_HTS_Ducted_Fan.py +# +# Created: Mar 2020, K. Hamilton +# Modified: Nov 2021, S. Claridge + +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- + +# suave imports +import SUAVE + +# package imports +import numpy as np +from SUAVE.Core import Data + + +from SUAVE.Components.Energy.Networks.Network import Network + +# ---------------------------------------------------------------------- +# Network +# ---------------------------------------------------------------------- + +## @ingroup Components-Energy-Networks +class Turboelectric_HTS_Ducted_Fan(Network): + """ A serial hybrid powertrain with partially superconducting propulsion motors where the superconducting field coils are energised by resistive current leads. + + Assumptions: + None + + Source: + None + """ + + def __defaults__(self): + """ This sets the default values for the network to function. + + Assumptions: + N/A + + Source: + N/A + + Inputs: + None + + Outputs: + None + + Properties Used: + N/A + """ + + self.leads = 2.0 # number of cryogenic leads supplying the rotor(s). Typically twice the number of rotors. + self.number_of_engines = 1.0 # number of ducted_fans, also the number of propulsion motors. + + self.engine_length = 1.0 + self.bypass_ratio = 0.0 + self.areas = Data() + self.tag = 'Turboelectric_HTS_Ducted_Fan' + + self.ambient_skin = False # flag to set whether the outer surface of the rotor is amnbient temperature or not. + self.skin_temp = 300.0 # [K] if self.ambient_skin is false, this is the temperature of the rotor skin. + + # manage process with a driver function + def evaluate_thrust(self, state): + + """ Calculate thrust given the current state of the vehicle + + Assumptions: + None + + Source: + N/A + + Inputs: + state [state()] + + Outputs: + results.thrust_force_vector [newtons] + results.vehicle_mass_rate [kg/s] + + Properties Used: + Defaulted values + """ + + # unpack + ducted_fan = self.ducted_fan # Electric ducted fan(s) excluding motor + motor = self.motor # Motor(s) driving those fans + powersupply = self.powersupply # Electricity producer(s) + esc = self.esc # Motor speed controller(s) + rotor = self.rotor # Rotor(s) of the motor(s) + lead = self.lead # Current leads supplying the rotor(s) + ccs = self.ccs # Rotor constant current supply + cryocooler = self.cryocooler # Rotor cryocoolers, powered by electricity + + ambient_skin = self.ambient_skin # flag to indicate rotor skin temp + rotor_surface_temp = self.skin_temp # Exterior temperature of the rotors + leads = self.leads # number of rotor leads, typically twice the number of rotors + number_of_engines = self.number_of_engines # number of propulsors and number of propulsion motors + number_of_supplies = self.powersupply.number_of_engines # number of turboelectric generators + + conditions = state.conditions + numerics = state.numerics + + amb_temp = conditions.freestream.temperature + + # Solve the thrust using the other network (i.e. the ducted fan network) + results = ducted_fan.evaluate_thrust(state) + + # Calculate the required electric power to be supplied to the ducted fan motor by dividing the shaft power required by the ducted fan by the efficiency of the ducted fan motor + # Note here that the efficiency must not include the efficiency of the rotor and rotor supply components as these are handled separately below. + + motor_power_in = ducted_fan.thrust.outputs.power/motor.motor_efficiency + + # Calculate the power used by the power electronics. This does not include the power delivered by the power elctronics to the fan motor. + esc_power = motor_power_in/esc.efficiency - motor_power_in + + # Set the rotor skin temp. Either it's ambient, or it's the temperature set in the rotor. + skin_temp = amb_temp * 1 + + if ambient_skin == False: + skin_temp[:] = rotor_surface_temp + + # If the rotor current is to be varied depending on the motor power here is the place to do it. For now the rotor current is set as constant. + rotor_currents = np.full_like(motor_power_in, rotor.current) + + # Calculate the power that must be supplied to the rotor. This also calculates the cryo load per rotor and stores this value as rotor.outputs.cryo_load + single_rotor_power = rotor.power(rotor_currents, skin_temp) + rotor_power_in = single_rotor_power * ducted_fan.number_of_engines + + # -------- Rotor Current Supply --------------------------------- + + # Calculate the power loss in the rotor current supply leads. + # The cryogenic loading due to the leads is also calculated here. + lead_power = np.where(rotor_currents[:,0] > 0, lead.Q_offdesign(rotor_currents[:,0])[:,1], 0.0 ) + lead_cryo_load = np.where(rotor_currents[:,0] > 0, lead.Q_offdesign(rotor_currents[:,0])[:,0], lead.unpowered_Q ) + + lead_power = np.reshape(lead_power, (len(lead_power),1)) + lead_cryo_load = np.reshape(lead_cryo_load, (len(lead_power),1)) + + # Multiply the lead powers by the number of leads, this is typically twice the number of motors + lead_power = lead_power * leads + lead_cryo_load = lead_cryo_load * leads + + # Calculate the power used by the rotor's current supply. + ccs_power = (lead_power+rotor_power_in)/ccs.efficiency - (lead_power+rotor_power_in) + + # Multiply the power (electrical and cryogenic) required by the rotor components by the number of rotors, i.e. the number of propulsion motors + all_leads_power = number_of_engines * lead_power + all_leads_cryo = number_of_engines * lead_cryo_load + all_ccs_power = number_of_engines * ccs_power + + # Retreive the cryogenic heat load from the rotor components (not including the leads). + rotor_cryo_cryostat = rotor.outputs.cryo_load * number_of_engines + + # Sum the two rotor cryogenic heat loads to give the total rotor cryogenic load. + rotor_cryo_load = rotor_cryo_cryostat + all_leads_cryo + + # Calculate the power required from the cryocoolers + cryocooler_load = rotor_cryo_load + cryocooler_power = cryocooler.energy_calc(cryocooler_load, rotor.temperature, amb_temp) + + # Sum all the power users to get the power required to be supplied by each powersupply, i.e. the turboelectric generators + powersupply.inputs.power_in = (motor_power_in + esc_power + rotor_power_in + all_leads_power + all_ccs_power + cryocooler_power) / number_of_supplies + + # Calculate the fuel mass flow rate at the turboelectric power supply. + fuel_mdot = number_of_supplies * powersupply.energy_calc(conditions, numerics) + + # Sum the mass flow rates and store this total as vehicle_mass_rate + results.vehicle_mass_rate = fuel_mdot + + # Pack up the mass flow rate components so they can be tracked. + results.vehicle_fuel_rate = fuel_mdot + + return results + + __call__ = evaluate_thrust diff --git a/trunk/SUAVE/Components/Energy/Networks/__init__.py b/trunk/SUAVE/Components/Energy/Networks/__init__.py index ade036ef8d..719f9442e2 100644 --- a/trunk/SUAVE/Components/Energy/Networks/__init__.py +++ b/trunk/SUAVE/Components/Energy/Networks/__init__.py @@ -22,4 +22,5 @@ from .Liquid_Rocket import Liquid_Rocket from .Internal_Combustion_Propeller_Constant_Speed import Internal_Combustion_Propeller_Constant_Speed from .PyCycle import PyCycle -from .Network import Network \ No newline at end of file +from .Network import Network +from .Turboelectric_HTS_Ducted_Fan import Turboelectric_HTS_Ducted_Fan diff --git a/trunk/SUAVE/Components/Energy/__init__.py b/trunk/SUAVE/Components/Energy/__init__.py index 8a2c681550..359d81a737 100644 --- a/trunk/SUAVE/Components/Energy/__init__.py +++ b/trunk/SUAVE/Components/Energy/__init__.py @@ -14,5 +14,6 @@ from . import Peripherals from . import Processes from . import Charging +from . import Cooling diff --git a/trunk/SUAVE/Methods/Power/Turboelectric/Discharge/__init__.py b/trunk/SUAVE/Methods/Power/Turboelectric/Discharge/__init__.py new file mode 100644 index 0000000000..f5c9629d88 --- /dev/null +++ b/trunk/SUAVE/Methods/Power/Turboelectric/Discharge/__init__.py @@ -0,0 +1,5 @@ +## @defgroup Methods-Power-Turboelectric-Discharge Discharge +# Functions to evaluate the use of a turboelectric powertrain to provide electric power. +# @ingroup Methods-Power-Turboelectric + +from .zero_fidelity import zero_fidelity diff --git a/trunk/SUAVE/Methods/Power/Turboelectric/Discharge/zero_fidelity.py b/trunk/SUAVE/Methods/Power/Turboelectric/Discharge/zero_fidelity.py new file mode 100644 index 0000000000..df6dc38d89 --- /dev/null +++ b/trunk/SUAVE/Methods/Power/Turboelectric/Discharge/zero_fidelity.py @@ -0,0 +1,35 @@ +## @ingroup Methods-Power-Turboelectric-Discharge +# zero_fidelity.py +# +# Created : Nov 2019, K. Hamilton +# Modified: Nov 2021, S. Claridge +# ---------------------------------------------------------------------- +# Zero Fidelity +# ---------------------------------------------------------------------- + +## @ingroup Methods-Power-Turboelectric-Discharge +def zero_fidelity(turboelectric,conditions,numerics): + ''' + Assumptions: + constant efficiency + + Inputs: + turboelectric. + inputs. + power_in [W] + propellant. + specific_energy [J/kg] + efficiency + + Outputs: + mdot [kg/s] + + ''' + + + power = turboelectric.inputs.power_in + + #mass flow rate of the fuel + mdot = power/(turboelectric.propellant.specific_energy*turboelectric.efficiency) + + return mdot diff --git a/trunk/SUAVE/Methods/Power/Turboelectric/Sizing/__init__.py b/trunk/SUAVE/Methods/Power/Turboelectric/Sizing/__init__.py new file mode 100644 index 0000000000..0d06c7ae4d --- /dev/null +++ b/trunk/SUAVE/Methods/Power/Turboelectric/Sizing/__init__.py @@ -0,0 +1,5 @@ +## @defgroup Methods-Power-Turboelectric-Sizing Sizing +# Turboelectric methods contain the functions for analyses where a turboelectric powertrain provieds the required electric power. +# @ingroup Methods-Power-Turboelectric + +from .initialize_from_power import initialize_from_power \ No newline at end of file diff --git a/trunk/SUAVE/Methods/Power/Turboelectric/Sizing/initialize_from_power.py b/trunk/SUAVE/Methods/Power/Turboelectric/Sizing/initialize_from_power.py new file mode 100644 index 0000000000..96373eb7a0 --- /dev/null +++ b/trunk/SUAVE/Methods/Power/Turboelectric/Sizing/initialize_from_power.py @@ -0,0 +1,55 @@ +## @ingroup Methods-Power-Turboelectric-Sizing + +# initialize_from_power.py +# +# Created : Nov 2019, K. Hamilton +# Modified: Nov 2021, S. Claridge + +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- +from SUAVE.Core import Units + +# ---------------------------------------------------------------------- +# Initialize from Power +# ---------------------------------------------------------------------- + +## @ingroup Methods-Power-Turboelectric-Sizing +def initialize_from_power(turboelectric,power,conditions): + ''' + assigns the mass of a single turboelectric generator based on the power and specific power + Assumptions: + Power output is derated relative to the air pressure. 100% power is considered a mean sea level. + + Inputs: + power [J] + turboelectric. + specific_power [W/kg] + conditions. + freestream_pressure [Pa] + + + Outputs: + turboelectric. + mass_properties. + mass [kg] + ''' + + # Unpack inputs + pressure = conditions.freestream.pressure + specific_power = turboelectric.specific_power + + # Ambient pressure as proportion of sealevel pressure, for use in derating the gas turbine + derate = pressure/101325. + + # Proportionally increase demand relative to the sizing altitude + demand_power = power/derate + + # Apply specific power specification to size individual powersupply + powersupply_mass = demand_power/specific_power + + # Store sized turboelectric rated power + turboelectric.rated_power = power + + # Modify turboelectric to include the newly created mass data + turboelectric.mass_properties.mass = powersupply_mass diff --git a/trunk/SUAVE/Methods/Power/Turboelectric/__init__.py b/trunk/SUAVE/Methods/Power/Turboelectric/__init__.py new file mode 100644 index 0000000000..8c011732a3 --- /dev/null +++ b/trunk/SUAVE/Methods/Power/Turboelectric/__init__.py @@ -0,0 +1,5 @@ +## @defgroup Methods-Power-Turboelectric Turboelectric +# Turboelectric methods contain the functions for investigating vehicle electric power supplied by a turboelectric powertrain. Created by modifying existing Fuel_Cell methods. +# @ingroup Methods-Power +from . import Discharge +from . import Sizing \ No newline at end of file diff --git a/trunk/SUAVE/Methods/Power/__init__.py b/trunk/SUAVE/Methods/Power/__init__.py index 6025221fb6..76441f5f98 100644 --- a/trunk/SUAVE/Methods/Power/__init__.py +++ b/trunk/SUAVE/Methods/Power/__init__.py @@ -3,4 +3,5 @@ # @ingroup Methods from . import Battery -from . import Fuel_Cell \ No newline at end of file +from . import Fuel_Cell +from . import Turboelectric diff --git a/trunk/SUAVE/Methods/Propulsion/__init__.py b/trunk/SUAVE/Methods/Propulsion/__init__.py index ddcc5f0927..73e9551e0f 100644 --- a/trunk/SUAVE/Methods/Propulsion/__init__.py +++ b/trunk/SUAVE/Methods/Propulsion/__init__.py @@ -4,7 +4,7 @@ from .ducted_fan_sizing import ducted_fan_sizing from .propeller_design import propeller_design -from .turbofan_emission_index import turbofan_emission_index +from .turbofan_emission_index import turbofan_emission_index from .electric_motor_sizing import size_from_kv, size_from_mass from .turbofan_sizing import turbofan_sizing from .turbojet_sizing import turbojet_sizing @@ -15,4 +15,5 @@ from .rayleigh import rayleigh from .nozzle_calculations import exit_Mach_shock, mach_area, normal_shock, pressure_ratio_isentropic, pressure_ratio_shock_in_nozzle from . import electric_motor_sizing -from .liquid_rocket_sizing import liquid_rocket_sizing \ No newline at end of file +from .liquid_rocket_sizing import liquid_rocket_sizing +from .serial_HTS_turboelectric_sizing import serial_HTS_turboelectric_sizing diff --git a/trunk/SUAVE/Methods/Propulsion/serial_HTS_turboelectric_sizing.py b/trunk/SUAVE/Methods/Propulsion/serial_HTS_turboelectric_sizing.py new file mode 100644 index 0000000000..8a634e82ad --- /dev/null +++ b/trunk/SUAVE/Methods/Propulsion/serial_HTS_turboelectric_sizing.py @@ -0,0 +1,268 @@ +## @ingroup Methods-Propulsion +# serial_HTS_turboelectric_sizing.py +# +# Created: Mar 2020, K. Hamilton +# Modified: Nov 2021, S. Claridge +# + +# ---------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------- +import SUAVE +import numpy as np +from SUAVE.Core import Data +from SUAVE.Methods.Power.Turboelectric.Sizing.initialize_from_power import initialize_from_power + + +## @ingroup Methods-Propulsion +def serial_HTS_turboelectric_sizing(Turboelectric_HTS_Ducted_Fan,mach_number = None, altitude = None, delta_isa = 0, conditions = None, cryo_cold_temp = 50.0, cryo_amb_temp = 300.0): + """create and evaluate a serial hybrid network that follows the power flow: + Turboelectric Generators -> Motor Drivers -> Electric Poropulsion Motors + where the electric motors have cryogenically cooled HTS rotors that follow the power flow: + Turboelectric Generators -> Current Supplies -> HTS Rotor Coils + and + Turboelectric Generators -> Cryocooler <- HTS Rotor Heat Load + There is also the capability for the HTS components to be cryogenically cooled using liquid or gaseous cryogen, howver this is not sized other than applying a factor to the cryocooler required power. + + Assumptions: + One powertrain model represents all engines in the model. + There are no transmission losses between components + the shaft torque and power required from the fan is the same as what would be required from the fan of a turbofan engine. + + Source: + N/A + + Inputs: + Turboelectric_HTS_Ducted_Fan Serial HTYS hybrid ducted fan network object (to be modified) + mach_number + altitude [meters] + delta_isa temperature difference [K] + conditions ordered dict object + + Outputs: + N/A + + Properties Used: + N/A + """ + + # Unpack components + ducted_fan = Turboelectric_HTS_Ducted_Fan.ducted_fan # Propulsion fans + motor = Turboelectric_HTS_Ducted_Fan.motor # Propulsion fan motors + turboelectric = Turboelectric_HTS_Ducted_Fan.powersupply # Electricity providers + esc = Turboelectric_HTS_Ducted_Fan.esc # Propulsion motor speed controllers + rotor = Turboelectric_HTS_Ducted_Fan.rotor # Propulsion motor HTS rotors + current_lead = Turboelectric_HTS_Ducted_Fan.lead # HTS rotor current supply leads + ccs = Turboelectric_HTS_Ducted_Fan.ccs # HTS rotor constant current supplies + cryocooler = Turboelectric_HTS_Ducted_Fan.cryocooler # HTS rotor cryocoolers + + + # Dummy values for specifications not currently used for analysis + motor_current = 100.0 + + # check if altitude is passed or conditions is passed + if(conditions): + # use conditions + pass + + else: + # check if mach number and temperature are passed + if(mach_number==None or altitude==None): + + # raise an error + raise NameError('The sizing conditions require an altitude and a Mach number') + + else: + + + # call the atmospheric model to get the conditions at the specified altitude + atmosphere = SUAVE.Analyses.Atmospheric.US_Standard_1976() + atmo_data = atmosphere.compute_values(altitude,delta_isa) + planet = SUAVE.Attributes.Planets.Earth() + + p = atmo_data.pressure + T = atmo_data.temperature + rho = atmo_data.density + a = atmo_data.speed_of_sound + mu = atmo_data.dynamic_viscosity + + # setup conditions + conditions = SUAVE.Analyses.Mission.Segments.Conditions.Aerodynamics() + + # freestream conditions + conditions.freestream.altitude = np.atleast_1d(altitude) + conditions.freestream.mach_number = np.atleast_1d(mach_number) + conditions.freestream.pressure = np.atleast_1d(p) + conditions.freestream.temperature = np.atleast_1d(T) + conditions.freestream.density = np.atleast_1d(rho) + conditions.freestream.dynamic_viscosity = np.atleast_1d(mu) + conditions.freestream.gravity = np.atleast_1d(planet.compute_gravity(altitude) ) + conditions.freestream.isentropic_expansion_factor = np.atleast_1d(ducted_fan.working_fluid.compute_gamma(T,p)) + conditions.freestream.Cp = np.atleast_1d(ducted_fan.working_fluid.compute_cp(T,p)) + conditions.freestream.R = np.atleast_1d(ducted_fan.working_fluid.gas_specific_constant) + conditions.freestream.speed_of_sound = np.atleast_1d(a) + conditions.freestream.velocity = conditions.freestream.mach_number*conditions.freestream.speed_of_sound + + # propulsion conditions + conditions.propulsion.throttle = np.atleast_1d(1.0) + + # Setup Components + ram = ducted_fan.ram + inlet_nozzle = ducted_fan.inlet_nozzle + fan = ducted_fan.fan + fan_nozzle = ducted_fan.fan_nozzle + thrust = ducted_fan.thrust + + bypass_ratio = ducted_fan.bypass_ratio #0 + number_of_engines = Turboelectric_HTS_Ducted_Fan.number_of_engines + + # Creating the network by manually linking the different components + + # set the working fluid to determine the fluid properties + ram.inputs.working_fluid = ducted_fan.working_fluid + + # Flow through the ram , this computes the necessary flow quantities and stores it into conditions + ram(conditions) + + # link inlet nozzle to ram + inlet_nozzle.inputs = ram.outputs + + # Flow through the inlet nozzle + inlet_nozzle(conditions) + + # Link the fan to the inlet nozzle + fan.inputs = inlet_nozzle.outputs + + # flow through the fan + fan(conditions) + + # link the fan nozzle to the fan + fan_nozzle.inputs = fan.outputs + + # flow through the fan nozzle + fan_nozzle(conditions) + + # compute the thrust using the thrust component + + # link the thrust component to the fan nozzle + thrust.inputs.fan_exit_velocity = fan_nozzle.outputs.velocity + thrust.inputs.fan_area_ratio = fan_nozzle.outputs.area_ratio + thrust.inputs.fan_nozzle = fan_nozzle.outputs + thrust.inputs.number_of_engines = number_of_engines + thrust.inputs.bypass_ratio = bypass_ratio + thrust.inputs.total_temperature_reference = fan_nozzle.outputs.stagnation_temperature + thrust.inputs.total_pressure_reference = fan_nozzle.outputs.stagnation_pressure + thrust.inputs.flow_through_core = 0. + thrust.inputs.flow_through_fan = 1. + + # nonexistant components used to run thrust + thrust.inputs.core_exit_velocity = 0. + thrust.inputs.core_area_ratio = 0. + thrust.inputs.core_nozzle = Data() + thrust.inputs.core_nozzle.velocity = 0. + thrust.inputs.core_nozzle.area_ratio = 0. + thrust.inputs.core_nozzle.static_pressure = 0. + + # compute the thrust + thrust.size(conditions) + mass_flow = thrust.mass_flow_rate_design + + # compute total shaft power required (i.e. the sum of the shaft power provided by all the fans) + shaft_power = fan.outputs.work_done * mass_flow + total_shaft_power = shaft_power * number_of_engines + Turboelectric_HTS_Ducted_Fan.design_shaft_power = total_shaft_power + + # update the design thrust value + ducted_fan.design_thrust = thrust.total_design + + # compute the sls_thrust + + # call the atmospheric model to get the conditions at the specified altitude + atmosphere_sls = SUAVE.Analyses.Atmospheric.US_Standard_1976() + atmo_data = atmosphere_sls.compute_values(0.0,0.0) + + p = atmo_data.pressure + T = atmo_data.temperature + rho = atmo_data.density + a = atmo_data.speed_of_sound + mu = atmo_data.dynamic_viscosity + + # setup conditions + conditions_sls = SUAVE.Analyses.Mission.Segments.Conditions.Aerodynamics() + + # freestream conditions + conditions_sls.freestream.altitude = np.atleast_1d(0.) + conditions_sls.freestream.mach_number = np.atleast_1d(0.01) + conditions_sls.freestream.pressure = np.atleast_1d(p) + conditions_sls.freestream.temperature = np.atleast_1d(T) + conditions_sls.freestream.density = np.atleast_1d(rho) + conditions_sls.freestream.dynamic_viscosity = np.atleast_1d(mu) + conditions_sls.freestream.gravity = np.atleast_1d(planet.sea_level_gravity) + conditions_sls.freestream.isentropic_expansion_factor = np.atleast_1d(ducted_fan.working_fluid.compute_gamma(T,p)) + conditions_sls.freestream.Cp = np.atleast_1d(ducted_fan.working_fluid.compute_cp(T,p)) + conditions_sls.freestream.R = np.atleast_1d(ducted_fan.working_fluid.gas_specific_constant) + conditions_sls.freestream.speed_of_sound = np.atleast_1d(a) + conditions_sls.freestream.velocity = conditions_sls.freestream.mach_number * conditions_sls.freestream.speed_of_sound + + # propulsion conditions + conditions_sls.propulsion.throttle = np.atleast_1d(1.0) + + state_sls = Data() + state_sls.numerics = Data() + state_sls.conditions = conditions_sls + results_sls = ducted_fan.evaluate_thrust(state_sls) + + Turboelectric_HTS_Ducted_Fan.sealevel_static_thrust = results_sls.thrust_force_vector[0,0] / number_of_engines + + # The shaft power is now known. + # To size the turboelectric generators the total required powertrain power is required. + # This is the sum of all the component s unpacked at the start. + # Each component sizing depends on the downstream component size. + # There are two streams: the main powertrain stream, and the cryogenic HTS rotor stream. + # Power conditioning out of the generator is considered as part of the turboelectric component. + # The two streams only join at this final stage. + + # Get total power required by the main powertrain stream by applying power loss of each component in sequence + # Each component is considered as one instance, i.e. one engine + motor_input_power = shaft_power/(motor.motor_efficiency * motor.gearbox_efficiency) + esc_input_power = esc.power(motor_current, motor_input_power) + drive_power = esc_input_power + + # Get power required by the cryogenic rotor stream + # The sizing conditions here are ground level conditions as this is highest cryocooler demand + HTS_current = np.array([rotor.current]) + rotor_input_power = rotor.power(HTS_current, rotor.skin_temp) + # initialize copper lead optimses the leads for the conditions set elsewhere, i.e. the lead is not sized here as it should be sized for the maximum ambient temperature + current_lead.initialize_material_lead() + current_lead_powers = current_lead.Q_offdesign( HTS_current) + lead_power = current_lead_powers[0,1] + leads_power = 2 * lead_power # multiply lead loss by number of leads to get total loss + ccs_output_power = leads_power + rotor_input_power + ccs_input_power = ccs.power(HTS_current, ccs_output_power) + # The cryogenic components are also part of the rotor power stream + lead_cooling_power = current_lead_powers[0,0] + leads_cooling_power = 2 * lead_cooling_power # multiply lead cooling requirement by number of leads to get total cooling requirement + total_lead_cooling_power = leads_cooling_power * number_of_engines + rotor_cooling_power = rotor.outputs.cryo_load + cooling_power = rotor_cooling_power + leads_cooling_power # Cryocooler must cool both rotor and supply leads + cryocooler_input_power = 0.0 + + cryocooler_input_power = cryocooler.rated_power + rotor_power = ccs_input_power + cryocooler_input_power + + # Add power required by each stream + engine_power = drive_power + rotor_power + total_engine_power = engine_power * number_of_engines + + # Size the turboelectric generator(s) based total power requirement + turboelectric_output_power = total_engine_power / turboelectric.number_of_engines + initialize_from_power(turboelectric,turboelectric_output_power,conditions) + + # Pack up each component rated power into each component + # As this will be used for sizing the mass of these components the individual power is used + motor.rated_power = shaft_power + esc.rated_power = motor_input_power + esc.rated_current = HTS_current + ccs.rated_power = ccs_output_power + ccs.rated_current = HTS_current + turboelectric.rated_power = turboelectric_output_power diff --git a/trunk/SUAVE/Methods/__init__.py b/trunk/SUAVE/Methods/__init__.py index 8c9708a158..b229087a52 100644 --- a/trunk/SUAVE/Methods/__init__.py +++ b/trunk/SUAVE/Methods/__init__.py @@ -14,6 +14,8 @@ from . import Center_of_Gravity from . import Costs + + from .skip import skip