def test_data_types(): """ Test that input of different data types consistently produces same output See: https://github.com/aarondettmann/ambiance/issues/1 """ # ------------------------------ # ----- Single value input ----- # ------------------------------ height = 11000 entry = table_data.property_dict[height] h_types = [ int(height), float(height), [int(height)], np.array(height, dtype=int), np.array(height, dtype=float), ] for h in h_types: print(repr(h)) for prop_name, value in entry.items(): computed = getattr(Atmosphere(h), prop_name) assert computed == approx(value, 1e-3) # ----------------------------------- # ----- Vector-like value input ----- # ----------------------------------- heights = [11000, 20000, 75895] entries = [table_data.property_dict[height] for height in heights] exp_values = defaultdict(list) for entry in entries: for prop_name in table_data.PROPERTY_NAMES: exp_values[prop_name].append(entry[prop_name]) h_types = [ list(int(height) for height in heights), tuple(int(height) for height in heights), list(float(height) for height in heights), tuple(float(height) for height in heights), np.array(heights, dtype=int), np.array(heights, dtype=float), ] for h in h_types: print(repr(h)) for prop_name, value in exp_values.items(): computed = getattr(Atmosphere(h), prop_name) assert computed == approx(value, 1e-3)
def estol(fc_s, W, S, h, CLmax): sigma = Atmosphere(h).density[0] / Atmosphere(0).density[0] omega_resp, V_resp = [], [] for i in fc_s: V_s = (2 * i * (W / S) / (Atmosphere(0).density[0] * sigma * CLmax))**(0.5) omega_s = 9.81 / V_s * np.sqrt(i**2 - 1) V_resp.append(V_s) omega_resp.append(omega_s) return V_resp, omega_resp
def calculate_cl(ref_area, alt, mach, mass, load_fact=1.05): """Function to calculate the lif coefficient (cl) Function 'calculate_cl' return the lift coefficient value for the given input (Reference Area, Altitude, Mach Number, Mass and Load Factor) Source: * Demonstration can be found in: /CEASIOMpy/lib/CLCalculator/doc/Calculate_CL.pdf Args: ref_area (float): Reference area [m^2] alt (float): Altitude [m] mach (float): Mach number [-] mass (float): Aircraft mass [kg] load_fact (float): Load Factor [-] (1.05 by default) Returns: target_cl (float): Lift coefficient [unit] """ # Get atmosphere values at this altitude Atm = Atmosphere(alt) GAMMA = 1.401 # Air heat capacity ratio [-] # Calculate lift coefficient weight = mass * Atm.grav_accel[0] dyn_pres = 0.5 * GAMMA * Atm.pressure[0] * mach**2 target_cl = weight * load_fact / (dyn_pres * ref_area) log.info(f"A lift coefficient (CL) of {target_cl} has been calculated") return target_cl
def init(): global seaLevelTemperature, seaLevelPressure from ambiance import Atmosphere seaLevelAtmosphere = Atmosphere(0) seaLevelPressure = seaLevelAtmosphere.pressure[0] seaLevelTemperature = seaLevelAtmosphere.temperature[0]
def test_sealevel(): """Test sealevel conditions""" sealevel = Atmosphere(0) # Geopotential and geometric height are equal assert sealevel.H == sealevel.h == 0 # Table 1 assert sealevel.temperature == 288.15 assert sealevel.temperature_in_celsius == 15 assert sealevel.pressure == 1.01325e5 assert sealevel.density == approx(1.225) assert sealevel.grav_accel == 9.80665 # Table 2 assert sealevel.speed_of_sound == approx(340.294) assert sealevel.dynamic_viscosity == approx(1.7894e-5, 1e-4) assert sealevel.kinematic_viscosity == approx(1.4607e-5, 1e-4) assert sealevel.thermal_conductivity == approx(2.5343e-2, 1e-4) # Table 3 assert sealevel.pressure_scale_height == approx(8434.5, 1e-4) assert sealevel.specific_weight == approx(1.2013e1, 1e-4) assert sealevel.number_density == approx(2.5471e25, 1e-4) assert sealevel.mean_particle_speed == approx(458.94, 1e-4) assert sealevel.collision_frequency == approx(6.9193e9, 1e-4) assert sealevel.mean_free_path == approx(6.6328e-8, 1e-4)
def drag(V, h, fc): ''' Arrasto em curva coordenada. ''' D_list = [] rho = Atmosphere(h).density[0] drag_manobra.Mp = V / Atmosphere(h).speed_of_sound[0] for i in fc: drag_manobra.CLp = CL(i, V, h) CD = drag_manobra.polar() D = (1 / 2) * rho * (V**2) * jet.S * CD D_list.append(D) return D_list
def standard_atm(h): """Compute quantities from International Civil Aviation Organization (ICAO) which extends the US 1976 Standard Atmospheric Model to 80 km. :h: altitude :returns: h_geop, T_inf, p_inf, rho_inf, a_inf, nu_inf """ h_meters = h.to('m').magnitude atm = Atmosphere(h_meters) # output units in SI arr_len = len(atm.H) if arr_len == 1: h_geop = atm.H[0] * unit('m') T_inf = atm.temperature[0] * unit('K') p_inf = atm.pressure[0] * unit('Pa') rho_inf = atm.density[0] * unit('kg/m^3') a_inf = atm.speed_of_sound[0] * unit('m/s') nu_inf = atm.kinematic_viscosity[0] * unit('m^2/s') else: h_geop = atm.H * unit('m') T_inf = atm.temperature * unit('K') p_inf = atm.pressure * unit('Pa') rho_inf = atm.density * unit('kg/m^3') a_inf = atm.speed_of_sound * unit('m/s') nu_inf = atm.kinematic_viscosity * unit('m^2/s') return h_geop, T_inf, p_inf, rho_inf, a_inf, nu_inf
def cruise_range(cond, V1, h, c, zeta, W): ''' OBS: Inutilizado! (remover antes de entregar) Parâmetros ---------- cond : Tipo de voo de cruzeiro, especificando qual variável se manterá constante: Velocidade (V), h (altitude) ou CL; V1 : Velocidade de início; h : Altitude de cruzeiro; c : Coeficiente de consumo; zeta : Razão W1/W2 dos pesos inicial e final. Returns ------- x : Alcance, em metros. ''' drag_cru = DragPolar() rho = Atmosphere(h).density[0] CL_cru = (2 * W) / (rho * V1**2 * jet.S) drag_cru.CLp = CL_cru drag_cru.Mp = V1 / Atmosphere(h).speed_of_sound[0] CD1 = drag_cru.polar() E1 = CL_cru / CD1 Em = 4 * drag_cru.K / drag_cru.CD0 if (cond == 'h_CL'): x = (2 * V1 * E1) / c * (1 - np.sqrt(1 - zeta)) elif (cond == 'V_CL'): x = E1 * V1 / c * np.log(1 / (1 - zeta)) elif (cond == 'V_h'): x = (2 * V1 * Em) / c * np.arctan( E1 * zeta / (2 * Em * (1 - drag_cru.K * E1 * CL_cru * zeta))) return x
def test_set_height_after_instantiating(): """Do not allow to set heights (h, H) after instantiating""" atmos = Atmosphere(0) attr_errors = [ 0, 1.2, np.array([1, 2, 3]), ] for invalid_input in attr_errors: with pytest.raises(AttributeError): atmos.h = invalid_input with pytest.raises(AttributeError): atmos.H = invalid_input
def test_geom_geop_height_conversion(): """Test conversion between geometric and geopotential height""" # Test different inputs inputs = [ np.arange(-5e3, 80e3, 1e3), (-5e3, 80e3, 1e3), [-5e3, 80e3, 1e3], -5000, ] for in_data in inputs: geom_height_in = np.arange(-5e3, 80e3, 1e3) geop_height_out = Atmosphere.geom2geop_height(geom_height_in) geom_height_out = Atmosphere.geop2geom_height(geop_height_out) assert np.testing.assert_allclose(geom_height_out, geom_height_in) is None
def check_phase(self, debug=False): """Check what phase of flight the rocket is in, e.g. on the rail, off the rail, or with the parachute open. Notes: - Since this only checks after each time step, there may be a very short period where the rocket is orientated as if it is still on the rail, when it shouldn't be. - For this reason, it may look like the rocket leaves the rail at an altitude greater than the rail length. Args: debug (bool, optional): If True, a message is printed when the rocket leaves the rail. Defaults to False. Returns: list: List of events that happened in this step, for the data log. """ events = [] # Rail check if self.on_rail == True: # Check how far we've travelled - remember that the 'l' coordinate system has its origin at alt=0. rocket_pos_l = pos_i2l(self.pos_i, self.launch_site, self.time) launch_site_pos_l = np.array([0.0, 0.0, self.launch_site.alt]) flight_distance = np.linalg.norm(rocket_pos_l - launch_site_pos_l) # Check if we've left the rail yet if flight_distance >= self.launch_site.rail_length: self.on_rail = False events.append("Cleared rail") if debug == True: alt = pos_i2alt(self.pos_i, self.time) ambient_pressure = (Atmosphere(alt).pressure[0] * self.env_vars["pressure"]) thrust = ( self.motor.thrust(self.time) + (self.motor.ambient_pressure - ambient_pressure) * self.motor.exit_area) weight = 9.81 * self.mass_model.mass(self.time) print( "Cleared rail at t={:.2f} s with alt={:.2f} m and TtW={:.2f}" .format(self.time, alt, thrust / weight)) # Parachute check if self.parachute_deployed == False: if self.alt_poll_watch < self.time - self.alt_poll_watch_interval: current_alt = pos_i2alt(self.pos_i, self.time) if self.alt_record > current_alt: if debug == True: print("Parachute deployed at {:.2f} km at {:.2f} s". format( pos_i2alt(self.pos_i, self.time) / 1000, self.time)) events.append("Parachute deployed") self.parachute_deployed = True self.w_b = np.array([0, 0, 0]) else: self.alt_poll_watch = self.time self.alt_record = current_alt return events
def test_table_data_single_value_input(): """Test that data corresponds to tabularised data from 'Doc 7488/3'""" for h, entry in table_data.property_dict.items(): print("="*80) print(f"Testing {h} m ...") for prop_name, value in entry.items(): computed = float(getattr(Atmosphere(h), prop_name)) # print(f"--> ({prop_name}) computed: {computed:.5e}") # print(f"--> ({prop_name}) expected: {value:.5e}") assert computed == approx(value, 1e-3)
def test_table_data_matrix_input(): """ Test that matrix can be passed as input (instead of single values) """ # "Sorted" matrices heights, properties = table_data.get_matrices() atmos = Atmosphere(heights) for prop_name, exp_values in properties.items(): computed_values = getattr(atmos, prop_name) assert np.testing.assert_allclose( computed_values, exp_values, rtol=1e-3) is None # Random matrices heights, properties = table_data.get_matrices(return_random=True) atmos = Atmosphere(heights) for prop_name, exp_values in properties.items(): computed_values = getattr(atmos, prop_name) print(prop_name) print(computed_values) print() print(exp_values) print("--------------") assert np.testing.assert_allclose( computed_values, exp_values, rtol=1e-3) is None # "Differently shaped" matrices heights, properties = table_data.get_matrices(shape=(4, 5)) atmos = Atmosphere(heights) for prop_name, exp_values in properties.items(): computed_values = getattr(atmos, prop_name) print(prop_name) print(computed_values) print() print(exp_values) print("--------------") assert np.testing.assert_allclose( computed_values, exp_values, rtol=1e-3) is None
def test_table_data_vector_input(): """Test that vector can be passed as input (instead of single values)""" # "Sorted" vectors heights, properties = table_data.get_vectors() atmos = Atmosphere(heights) for prop_name, exp_values in properties.items(): computed_values = getattr(atmos, prop_name) # print(computed_values) # print(exp_values) assert np.testing.assert_allclose(computed_values, exp_values, rtol=1e-3) is None # Random vectors heights, properties = table_data.get_vectors(return_random=True) atmos = Atmosphere(heights) print(heights) for prop_name, exp_values in properties.items(): computed_values = getattr(atmos, prop_name) assert np.testing.assert_allclose(computed_values, exp_values, rtol=1e-3) is None
def test_invalid_inputs(): """Do not allow strange input""" with pytest.raises(TypeError): Atmosphere() type_errors = [ None, dict, str, ] value_errors = [[], (), [[]], [1, [2, 3]]] for invalid_input in type_errors: with pytest.raises(TypeError): Atmosphere(invalid_input) for invalid_input in value_errors: with pytest.raises(ValueError): Atmosphere(invalid_input)
def payload_vs_range(c,POV,MTOW,max_payload,max_fuel, V1, h1): Mach = V1/Atmosphere(h1).speed_of_sound[0] # Ponto A: ''' A aeronave não possui combustível nesse ponto, não havendo variação de peso. ''' W1_A = POV + max_payload W2_A = W1_A zeta_A = (W1_A - W2_A)/W1_A x_A = cruise_range('V_h', W1_A, c, zeta_A, V1, h1) # Ponto B: ''' Nesse ponto, adiciona-se combustível até atingir-se o MTOW da aeronave. ''' W1_B = MTOW W2_B = W1_B - (MTOW - (max_payload + POV)) zeta_B = (W1_B - W2_B)/W1_B x_B = cruise_range('V_h',W1_B, c, zeta_B, V1, h1) # Ponto C: ''' Nesse ponto, partiu-se com o máximo combustível permitido. ''' W1_C = MTOW W2_C = W1_C - max_fuel zeta_C = (W1_C - W2_C)/W1_C x_C = cruise_range('V_h', W1_C, c, zeta_C, V1, h1) # Ponto D: ''' Nesse ponto, partiu-se sem carga paga, com o máximo de combustível. ''' W1_D = POV + max_fuel W2_D = POV zeta_D = (W1_D - W2_D)/W1_D x_D = cruise_range('V_h',W1_D, c, zeta_D, V1, h1) plt.figure() plt.ylabel("Payload [kg]", fontsize = 12) plt.xlabel("x [km]", fontsize = 12) plt.grid(False) plt.plot([x_A/1000,x_B/1000],[max_payload/9.81,max_payload/9.81],'-ok', linewidth = 3) plt.plot([x_B/1000, x_C/1000],[max_payload/9.81, (MTOW - (POV + max_fuel))/9.81],'-ok', linewidth = 3) plt.plot([x_C/1000, x_D/1000],[(MTOW - (POV + max_fuel))/9.81, 0],'-ok', linewidth = 3) plt.text(2000, 600,'M = {:.1f}\nh = {:.1f} km'.format(Mach,h1/1000)) plt.savefig("carga_paga.svg") plt.show() return
def add_forces(self, state, t): # Weight weight = -9.80665 * state.mass self._forces.append(weight) # Thrust thrust = self._motor.thrust_at(t) self._forces.append(thrust) # (Very) rough approximation of drag air_density = float(Atmosphere(state.pos).density) drag = 0.5 * self._frontal_area * air_density * -(state.vel**2.0) * self._drag_coefficient self._forces.append(drag)
def T(h, T0, n, fc): ''' Empuxo em curva coordenada. ''' T = [] for i in fc: sigma = Atmosphere(h).density[0] / rho0 Ti = T0 * (sigma**n) T.append(Ti) return T
def CL(fc, V, h): ''' Coeficiente de sustentação em curva coordenada. Entradas: h: Altitude (inteiro) V: Velocidade (lista) ''' rho = Atmosphere(h).density[0] CL = 2 * fc * jet.W / (rho * (V**2) * jet.S) return CL
def max_CL_cte(x, h1, h2, cond): rho = (Atmosphere(h1).density[0] + Atmosphere(h2).density[0]) / 2 velo_som = (Atmosphere(h1).speed_of_sound[0] + Atmosphere(h2).speed_of_sound[0]) / 2 # Variáveis desconhecidas: CD0 = x[0] k = x[1] V = x[2] drag_CL.Mp = V / velo_som drag_polar_CL = drag_CL.polar() if (cond.get('condicao') == 'max_range'): # CL de otimização do alcance: CL = np.sqrt(CD0 / k) # Equações do sistema: eq1 = (jet.W / (0.5 * CL * rho * jet.S))**.5 - V eq2 = drag_CL.CD0 - CD0 eq3 = drag_CL.K - k return [eq1, eq2, eq3] elif (cond.get('condicao') == 'max_endurance'): # CL de otimização da autonomia: CL = np.sqrt(3 * CD0 / k) # Equações do sistema: eq1 = (jet.W / (0.5 * CL * rho * jet.S))**.5 - V eq2 = drag_CL.CD0 - CD0 eq3 = drag_CL.K - k return [eq1, eq2, eq3]
def compute_other_aero_quantities(self): # Compute cruise speed, based on altitude and Mach during cruise V_sound = Atmosphere(int(self.h_cruise)).speed_of_sound[0] # h_cruise must be in meters self.V_cruise = round(self.M_cruise * V_sound, 1) # m/s # Compute (L/D)_max self.L_D_max = round( 0.5 * m.sqrt(m.pi * self.AR / (self.k * self.CD_0)), 2) # Compute load factor during loiter self.n_loiter = round(1 / m.cos(self.phi_loiter * 2 * m.pi / 360), 4)
def total_drag(V, h): ''' Parâmetros ---------- V : Lista de velocidades definida em "MAIN". h : Lista de altitudes definida em "MAIN". Returns ------- D_resp : Arrasto total em cruzeiro (lista) Dmin_resp : Arrasto mínimo em cruzeiro (lista) ''' D_resp, Dmin_resp = [], [] for i in h: sigma = Atmosphere(i).density[0] / rho0 drag_object.Mp = V / Atmosphere(i).speed_of_sound[0] drag_polar = drag_object.polar() CD0 = drag_object.CD0 K = drag_object.K Dp = (1 / 2) * sigma * rho0 * (V**2) * jet.S * CD0 Di = (2 * K * (jet.W**2)) / (sigma * rho0 * jet.S * V**2) D_total = Dp + Di Emax = np.sqrt(CD0 / K) / (2 * CD0) Dmin = jet.W / Emax D_resp.append(D_total) Dmin_resp.append(Dmin) return D_resp, Dmin_resp
def h_dot_max_eq(x, h, S, W, T0, n): ''' Montagem do sistema para calcular a velocidade de maxima razao de subida, dada uma altitude 'h'. Entradas: --------- h: Altitude (float) S: Area de asa (float) W: Peso da aeronave (float) T0, n: Parametros propulsivos (float) Saidas: ------- eq: Sistema de equacoes a serem resolvidas ''' CD0 = x[0] K = x[1] V = x[2] T = cr.jet_buoyancy(h, T0, n)[0] drag_asc.Mp = V/Atmosphere(h).speed_of_sound[0] drag_polar = drag_asc.polar() rho = Atmosphere(h).density[0] eq1 = drag_asc.CD0 - CD0 eq2 = drag_asc.K - K eq3 = ((T/S)/(3*rho*CD0)*(1 + np.sqrt(1 + 12*CD0*K*(W/T)**2)))**(1/2) - V eq = [eq1, eq2, eq3] return eq
def estol(W, S, h, CLmax): V_s_resp = [] if(type(h) == np.ndarray or type(h) == list): for i in h: sigma = Atmosphere(i).density[0]/Atmosphere(0).density[0] V_s = (2*(W/S)/(Atmosphere(0).density[0]*sigma*CLmax))**(0.5) V_s_resp.append(V_s) return V_s_resp else: sigma = Atmosphere(h).density[0]/Atmosphere(0).density[0] V_s = (2*(W/S)/(Atmosphere(0).density[0]*sigma*CLmax))**(0.5) return V_s sigma = Atmosphere(h).density[0]/Atmosphere(0).density[0] V_s = (2*(W/S)/(Atmosphere(0).density[0]*sigma*CLmax))**(0.5) return V_s
def get_drag(self, altitude, speed, engine_on): if altitude >= 81020: return (0) atmosphere = Atmosphere(altitude) mach = speed / atmosphere.speed_of_sound[0] density = atmosphere.density[0] if mach <= 0.01: mach = 0.01 if engine_on: cd = self.cd_on(mach) else: cd = self.cd_off(mach) drag_force = 0.5 * cd * density * (np.pi * self.radius**2) * (speed**2) return (drag_force)
def estimate_skin_friction_coef(wetted_area, wing_area, wing_span, mach, alt): """Return an estimation of skin friction drag coefficient. Function 'estimate_skin_friction_coef' gives an estimation of the skin friction drag coefficient, based on an empirical formala (see source). Source: * Gerard W. H. van Es. "Rapid Estimation of the Zero-Lift Drag Coefficient of Transport Aircraft", Journal of Aircraft, Vol. 39, No. 4 (2002), pp. 597-599. https://doi.org/10.2514/2.2997 Args: wetted_area (float): Wetted Area of the entire aircraft [m^2] wing_area (float): Main wing area [m^2] wing_span (float): Main wing span [m] mach (float): Cruise Mach number [-] alt (float): Aircraft altitude [m] Returns: cd0 (float): Drag coefficient due to skin friction [-] """ # Get atmosphere values at this altitude Atm = Atmosphere(alt) # Get speed from Mach Number speed = mach * Atm.speed_of_sound[0] # Reynolds number based on the ratio Wetted Area / Wing Span reynolds_number = (wetted_area / wing_span) * speed / Atm.kinematic_viscosity[0] log.info("Reynolds number:" + str(round(reynolds_number))) # Skin friction coefficient, formula from source (see function description) cfe = (0.00258 + 0.00102 * math.exp(-6.28 * 1e-9 * reynolds_number) + 0.00295 * math.exp(-2.01 * 1e-8 * reynolds_number)) log.info("Skin friction coefficient:" + str(round(cfe, 5))) # Drag coefficient due to skin friction cd0 = cfe * wetted_area / wing_area log.info("Skin friction drag coefficient: " + str(cd0)) return cd0
def setupInitialConditions(workingPath, alfa, beta, mach, altitude, transient): #PATHS templatePath = os.path.join(workingPath, settings.templatePath) filePath = os.path.join( workingPath, settings.filePathTransient if transient else settings.filePathSteady) #ATMOSPHERIC MODEL atm = Atmosphere(altitude) temp = atm.temperature[0] press = atm.pressure[0] dens = atm.density[0] c = atm.speed_of_sound[0] vel = c * mach u = rotateVelocity(vel, alfa, beta) #TURBULENCE PARAMETERS Re, turbulentLengthScale, turbulentIntensity, k, w, epsilon, nut, nuTilda = turbulenceCalculator( 0.15, vel) #OPEN TEMPLATE with open(templatePath, 'r') as f: dumpStr = f.read() #SUBSTITUTE PARAMETERS IN TEMPLATE dumpStr = dumpStr.replace("%ux%", str(round(u[0], 3))) dumpStr = dumpStr.replace("%uy%", str(round(u[1], 3))) dumpStr = dumpStr.replace("%uz%", str(round(u[2], 3))) dumpStr = dumpStr.replace("%pressure%", str(round(press, 3))) dumpStr = dumpStr.replace("%densityRef%", str(round(dens, 3))) dumpStr = dumpStr.replace("%temperature%", str(round(temp, 3))) dumpStr = dumpStr.replace("%nut%", str(round(nut, 5))) dumpStr = dumpStr.replace("%k%", str(round(k, 5))) dumpStr = dumpStr.replace("%w%", str(round(w, 5))) dumpStr = dumpStr.replace("%Uinf%", str(round(vel, 3))) dumpStr = dumpStr.replace("%nuTilda%", str(round(nuTilda, 5))) #DUMP FILE with open(filePath, 'w') as f: f.write(dumpStr) return True
def jet_buoyancy(h,T0,n): ''' Parâmetros ---------- h : Lista de altitudes definida em "MAIN". T0 : Empuxo dos quatro motores da aeronave ao nível do mar n: Coeficiente propulsivo Returns ------- T : Lista de empuxo para cada altitude (h). ''' T = [] for i in h: sigma = Atmosphere(i).density[0]/rho0 Ti = T0*(sigma**n) T.append(Ti) return T
def h_vs_V(h,V1,V2,Vs): V_som = Atmosphere(h).speed_of_sound h_plot = [i*3.28084 for i in h] plt.style.use('tableau-colorblind10') plt.figure() plt.xlabel("Mach") plt.ylabel("h [ft]") plt.grid(True) plt.plot(V1/V_som,h_plot,'k') plt.plot(V2/V_som,h_plot,'k') plt.plot(Vs/V_som, h_plot, 'r', label = 'Stall') plt.legend(loc = 'best', framealpha = 1) plt.savefig('h_vs_V.svg') plt.show() return
def make_plots(): h = np.linspace(CONST.h_min, CONST.h_max, num=1000) a = Atmosphere(h) hs = h/1000 lw = 1.5 cp1 = 'blue' cp2 = 'green' for p in props: fig, ax1 = plt.subplots() data = getattr(a, p.name) ax1.plot(data, hs, lw=lw, c=cp1) ax1.set_ylabel('Height [km]') ax1.tick_params(axis='x', labelcolor=cp1) ax1.grid() ax1.set_xlabel(f"{p.name_long} [{p.unit}]") if p.log: ax2 = ax1.twiny() ax2.plot(data, hs, '--', lw=lw, c=cp2) ax2.set_xscale('log') ax2.tick_params(axis='x', labelcolor=cp2) ax2.grid() fig.tight_layout() fname = p.name + '.png' print(fname) plt.savefig(os.path.join(HERE, fname)) plt.cla() plt.close('all') plt.clf()