PyFoil 1.1
Sto continuando lo sviluppo dell’applicazione presentata in un precedente articolo, PyFoil.
PyFoil è scritto in Python per dispositivi mobili Symbian. Per utilizzarlo è necessario installare PyS60 sul proprio cellulare.
La versione precedente a questa era in grado solo di disegnare profili alari, in questa versione ho migliorato questa funzione e ne ho aggiunte di nuove.
L’applicazione è divisa in quattro schede:
- Intro: è una semplice scheda di introduzione sul programma
- Plot: permette di disegnare un NACA a 4 o 5 cifre e di esportare l’immagine in un file
- Group: permette il calcolo di alcuni gruppi adimensionali quali Reynolds, Mach e Froude, a partire da diversi parametri
- ISA: restituisce i parametri dell’Atmosfera Standard in base all’altitudine, espressa in metri o piedi.
Prossimi sviluppi: l’obiettivo è quello di creare un’applicazione che possa essere da supporto (mobile) ad un ingegnere aerospaziale. Le prossime funzioni riguarderanno: la risoluzione del campo di moto attorno ai profili con relative informazioni connesse; aumento del numero di gruppi adimensionali calcolabili; calcolo di informazioni relative all’ala.
Bug noti: il calcolo di densità e pressione nell’ISA utilizza la stessa funzione sia per la troposfera che per la stratosfera, che è un errore. Purtroppo la formula del calcolo in stratosfera mi dava qualche errore e ho dovuto fare questa semplificazione che risolverò nella prossima versione.
Alcuni screenshot:
Segue il codice del programma.
import e32
import graphics
import appuifw
from random import randint
from math import sqrt, sin, cos, tan, atan, log10, ceil, pi, e as E
def draw(r=None):
"""Draw the buffer on the canvas"""
if buffer:
c.blit(buffer)
c = appuifw.Canvas(redraw_callback=draw)
buffer = graphics.Image.new(c.size)
appuifw.app.body = c
width, height = c.size
# N contains geometrical parameter of airfoil
airfoil = N = digit = altitude = None
def derivative(func, x, par=None):
"""Derivates a function (par is an additional parameter to derivate
functions like mean_line(x, N))"""
if par:
return (func(x, par) - func(x+0.01, par))/0.01
else:
return (func(x) - func(x+0.01))/0.01
def mean_line(x, N):
"""Airfoil mean line function (N contains NACA parameters)"""
t, m, p = N
if digit == 4:
if p == 0:
return 0
elif x <= p: return m / (p**2) * (2*p*x - x**2) elif x > p:
return m * (1 - 2*p + 2*p*x - x**2) / ((1 - p)**2)
elif digit == 5:
if x <= m: return p / 6 * (x**3 - 3*m*x**2 + m**2 * (3 - m)*x) elif x > m:
return p * m**3 / 6*(1 - x)
def thickness(x, N):
"""Airfoil thickness function (N contains NACA parameters)"""
t, m, p = N
return t / 0.2 * (+0.2969 * x**0.5 +
-0.1260 * x**1 +
-0.3516 * x**2 +
+0.2843 * x**3 +
-0.1015 * x**4)
def NACA_set():
"""Set NACA to operate"""
global airfoil, digit, N
airfoil = appuifw.query(u'Insert 4-5 digit NACA', 'number')
if airfoil:
digit = ceil(log10(airfoil))
# Digit correction for 00XX and 000X
if digit in [1, 2]: digit = 4
t = (airfoil%100)/100. # Thickness
if digit == 4:
m = (airfoil/1000)/100. # Max camber
p = (airfoil/100-airfoil/1000*10)/10. # Max camber position
N = (t, m, p)
NACA_plot()
elif digit == 5:
mean_line_datas = {210:[0.0580, 361.4],
220:[0.1260, 51.64],
230:[0.2025, 15.957],
240:[0.2900, 6.643],
250:[0.3910, 3.230]}
try:
m = mean_line_datas[airfoil/100][0]
p = mean_line_datas[airfoil/100][1]
N = (t, m, p)
NACA_plot()
except KeyError:
appuifw.note(u'NACA not supported!', 'error')
else:
appuifw.note(u'NACA must be 4 or 5 digit!', 'error')
else:
appuifw.note(u'NACA must be 4 or 5 digit!', 'error')
def NACA_plot():
"""Plots a NACA"""
if not airfoil:
NACA_set()
if not airfoil:
return
buffer.clear()
font = (None, 30)
color0 = (0, 0, 0) # Text color
color1 = (0, 0, 255) # Airfoil color
color2 = (255, 0, 0) # Meanline color
color3 = (100, 100, 100) # Radius color
# Draws the axes
s_width = width - 10 # Scaled width, for the border
y0 = height/2 # Origin of axes
buffer.line((0, y0, width, y0), outline=color0)
# Draws the scale
unit = 10 # Axis will be divided into %unit part
for u in range(11):
buffer.line((5 + u * s_width / unit, y0 - 2,
5 + u * s_width / unit, y0 + 2),
outline=color0)
# Unit legend
buffer.line((10, 2*y0 - 20,
10 + s_width/unit, 2*y0 - 20),
outline=color0)
buffer.text((20 + s_width/unit, 2*y0 - 15),
u'%d%% of the chord' % (100/unit),
fill=color0)
# Displays infos about the airfoil
radius = 1.1019 * N[0]**2
radius_pos = (5, y0 - radius*s_width,
5 + radius*s_width*2, y0 + radius*s_width)
buffer.ellipse(radius_pos, outline=color3)
buffer.text((10, 30), u'NACA %0#4d' % airfoil, font=font, fill=color0)
buffer.text((10, 55), u'Camber radius: %.4f' % radius, fill=color0)
# Plot
for x in range(s_width):
x = float(x)/s_width
# xx is an increment of x to calculate next point
xx = (x * s_width + 1) / s_width
# Meanline
xM_1 = 5 + x * s_width
yM_1 = y0 - mean_line(x, N) * s_width
xM_2 = 5 + xx * s_width
yM_2 = y0 - mean_line(xx, N) * s_width
buffer.line((xM_1, yM_1, xM_2, yM_2), outline=color2)
# Airfoil (U: Upper, L: Lower, 1-2 are 1st and 2nd point of the line)
teta = atan(derivative(mean_line, x, N))
xU_1 = 5 + (x - thickness(x, N)*sin(teta)) * s_width
yU_1 = y0 - (mean_line(x, N) - thickness(x, N)*cos(teta)) * s_width
xL_1 = 5 + (x + thickness(x, N)*sin(teta)) * s_width
yL_1 = y0 - (mean_line(x, N) + thickness(x, N)*cos(teta)) * s_width
xU_2 = 5 + (xx - thickness(xx, N)*sin(teta)) * s_width
yU_2 = y0 - (mean_line(xx, N) - thickness(xx, N)*cos(teta)) * s_width
xL_2 = 5 + (xx + thickness(xx, N)*sin(teta)) * s_width
yL_2 = y0 - (mean_line(xx, N) + thickness(xx, N)*cos(teta)) * s_width
buffer.line((xU_1, yU_1, xU_2, yU_2), outline=color1, width=2)
buffer.line((xL_1, yL_1, xL_2, yL_2), outline=color1, width=2)
draw()
def NACA_export():
"""Export NACA plot as image"""
if not airfoil:
NACA_set()
if not airfoil:
return
new_width = appuifw.query(u'Image width (px)', 'number', 800)
new_height = new_width / 1.4
image = graphics.Image.new((new_width, new_height))
image.clear()
font = (None, 30)
color0 = (0, 0, 0) # Text color
color1 = (0, 0, 255) # Airfoil color
color2 = (255, 0, 0) # Meanline color
color3 = (100, 100, 100) # Radius color
# Draws the axes
s_width = new_width - 10 # Scaled width, for the border
y0 = new_height/2 # Origin of axes
image.line((0, y0, new_width, y0), outline=color0)
# Draws the scale
unit = 10 # Axis will be divided into %unit part
for u in range(11):
image.line((5 + u * s_width / unit, y0 - 2,
5 + u * s_width / unit, y0 + 2),
outline=color0)
# Unit legend
image.line((10, 2*y0 - 20,
10 + s_width/unit, 2*y0 - 20),
outline=color0)
image.text((20 + s_width/unit, 2*y0 - 15),
u'%d%% of the chord' % (100/unit),
fill=color0)
# Displays infos about the airfoil
radius = 1.1019 * N[0]**2
radius_pos = (5, y0 - radius*s_width,
5 + radius*s_width*2, y0 + radius*s_width)
image.ellipse(radius_pos, outline=color3)
image.text((10, 30), u'NACA %0#4d' % airfoil, font=font, fill=color0)
image.text((10, 55), u'LE radius: %.4f' % radius, fill=color0)
# Plot
for x in range(s_width):
x = float(x)/s_width
# xx is an increment of x to calculate next point
xx = (x * s_width + 1) / s_width
# Meanline
xM_1 = 5 + x * s_width
yM_1 = y0 - mean_line(x, N) * s_width
xM_2 = 5 + xx * s_width
yM_2 = y0 - mean_line(xx, N) * s_width
image.line((xM_1, yM_1, xM_2, yM_2), outline=color2)
# Airfoil (U: Upper, L: Lower)
teta = atan(derivative(mean_line, x, N))
xU_1 = 5 + (x - thickness(x, N)*sin(teta)) * s_width
yU_1 = y0 - (mean_line(x, N) - thickness(x, N)*cos(teta)) * s_width
xL_1 = 5 + (x + thickness(x, N)*sin(teta)) * s_width
yL_1 = y0 - (mean_line(x, N) + thickness(x, N)*cos(teta)) * s_width
xU_2 = 5 + (xx - thickness(xx, N)*sin(teta)) * s_width
yU_2 = y0 - (mean_line(xx, N) - thickness(xx, N)*cos(teta)) * s_width
xL_2 = 5 + (xx + thickness(xx, N)*sin(teta)) * s_width
yL_2 = y0 - (mean_line(xx, N) + thickness(xx, N)*cos(teta)) * s_width
image.line((xU_1, yU_1, xU_2, yU_2), outline=color1, width=2)
image.line((xL_1, yL_1, xL_2, yL_2), outline=color1, width=2)
file_name = appuifw.query(u'Insert file name', 'text', u'.png')
file_path = u'C:\\%s' % file_name
image.save(file_path)
del image
appuifw.note(u'Image saved at C:\\%s' % file_name, 'info')
# Ask if want to send the file
if appuifw.query(u'Send the file via BT?', 'query'):
try:
import btsocket as socket
except ImportError:
import socket
address, services = socket.bt_obex_discover()
channel = services.items()[0][1]
try:
socket.bt_obex_send_file(address, channel, file_path)
except error:
appuifw.note(error.decode('utf-8'), 'error')
def velocity_field():
"""Solve the velocity field"""
appuifw.note(u'Coming soon!', 'info')
def ISA(z):
"""International standard atmosphere"""
T_sl = 288.15 # Kelvin
p_sl = 101325.0 # Pascal
rho_sl = 1.225 # kg/m^3
# Air gas constant: 287 J / (kg * K)
T = T_sl - 6.5 * (z/1000.0) # Thermal gradient: -6.5 K/km
p = p_sl * (T/T_sl) ** (9.81 / 287 / 6.5e-3)
rho = rho_sl * (T/T_sl) ** (9.81 / 287 / 6.5e-3 - 1)
# Troposphere
#if z < 11000: # T = T_sl - 6.5 * (z/1000.0) # Thermal gradient: -6.5 K/km # p = p_sl * (T/T_sl) ** (9.81 / 287 / 6.5e-3) # rho = rho_sl * (T/T_sl) ** (9.81 / 287 / 6.5e-3 - 1) ## Stratosphere #elif z >= 11000 and z < 20000: # T = 216.65 # p = 2270 * E ** (-9.81 / 287 / 6.5e-3 * (z-11000)) # rho = 0.2978 * E ** (-9.81 / 287 / 6.5e-3 * (z-11000)) #elif z >= 20000:
# # Up 20000 m thermal gradient is approximated
# T = 216.65 + 0.98 * (z-20000)/1000.0 # Thermal gradient: ~ 0.98 K/km
# p = 2270 * E ** (-9.81 / 287 / 6.5e-3 * (z-11000))
# rho = 0.2978 * E ** (-9.81 / 287 / 6.5e-3 * (z-11000))
return (T, p, rho)
def Reynolds():
"""Shows a form to calculate dimensionless quantity"""
fields = [(u'Speed [m/s]', 'float', 0.0),
(u'Density [kg/m^3]', 'float', 0.0),
(u'D. viscosity [Pa*s]', 'float', 0.0),
(u'Linear dimension [m]', 'float', 0.0)]
flag = appuifw.FFormEditModeOnly + appuifw.FFormDoubleSpaced
form = appuifw.Form(fields, flag)
form.execute()
# Result
speed, density, viscosity, linear_d = [i[2] for i in list(form)]
reynolds = density * speed * linear_d / viscosity
appuifw.query(u'Reynolds:', 'text', unicode(reynolds))
appuifw.query(u'Reynolds (exp):', 'text', u'%.0e' % reynolds)
def Mach():
"""Shows a form to calculate dimensionless quantity"""
fields = [(u'Speed [m/s]', 'float', 0.0),
(u'Sound speed [m/s]', 'float', 0.0),
(u'* Temperature [\u00B0C]', 'float', 0.0),
(u'* Altitude [m]', 'float', 0.0)]
flag = appuifw.FFormEditModeOnly + appuifw.FFormDoubleSpaced
form = appuifw.Form(fields, flag)
appuifw.note(u'You may use temp. or alt. instead of sound speed', 'info')
form.execute()
# Result
speed, sound_speed, temperature, altitude = [i[2] for i in list(form)]
if sound_speed == 0.0:
if temperature != 0.0:
# Air gas constant: 287 J / (kg * K)
sound_speed = (1.4 * 287 * (273.15+temperature)) ** 0.5
elif altitude != 0.0:
sound_speed = (1.4 * 287 * ISA(altitude)[0]) ** 0.5
else:
appuifw.note(u'Not enough parameters', 'error')
mach = speed / sound_speed
appuifw.query(u'Mach:', 'text', unicode(mach))
def Froude():
"""Shows a form to calculate dimensionless quantity"""
fields = [(u'Speed [m/s]', 'float', 0.0),
(u'\u0394 z [m]', 'float', 0.0),
(u'Gravity [m/s^2]', 'float', 9.81)]
flag = appuifw.FFormEditModeOnly + appuifw.FFormDoubleSpaced
form = appuifw.Form(fields, flag)
form.execute()
# Result
speed, linear_d, gravity = [i[2] for i in list(form)]
froude = speed * speed / gravity / linear_d
appuifw.query(u'Froude:', 'text', unicode(froude))
def rotate_screen():
"""Rotate the screen and rebuilt the canvas"""
global width, height, c, buffer
screen = appuifw.app.orientation
if screen == 'landscape':
appuifw.app.orientation = 'portrait'
else:
appuifw.app.orientation = 'landscape'
del c
c = appuifw.Canvas(redraw_callback=draw)
width, height = c.size
buffer = buffer.resize(c.size)
def set_altitude(um):
"""Set altitude (um is unit of measurement)"""
global altitude
if um == 'm':
altitude = appuifw.query(u'Insert altitude [m]:', 'float')
tab_3()
elif um == 'ft':
altitude_ft = appuifw.query(u'Insert altitude [ft]:', 'float')
altitude = altitude_ft * 0.3048
tab_3()
if altitude == None:
appuifw.note(u'Altitude not set!', 'error')
return
def quit():
e32.Ao_lock().signal()
def tab_0():
"""Starting graphics"""
buffer.clear()
color1 = (0, 0, 0)
color2 = (0, 255, 0)
color3 = (0, 150, 0)
font1 = (u'Nokia Hindi TitleSmBd S6', 30)
font2 = font3 = (u'Nokia Hindi TitleSmBd S6', 15)
text1 = u'NACA PyFoil'
text2 = u'By Ale152'
text3 = u'www.wirgilio.it'
box1 = buffer.measure_text(text1, font1)
box2 = buffer.measure_text(text2, font2)
box3 = buffer.measure_text(text3, font3)
position1 = ((width-box1[0][2])/2, 30)
position2 = ((width-box2[0][2])/2, 50)
position3 = ((width-box3[0][2])/2, 65)
buffer.text(position1, text1, font=font1, fill=color1)
buffer.text(position2, text2, font=font2, fill=color1)
buffer.text(position3, text3, font=font3, fill=color1)
s_width = width - 40 # Scaled width, for the border
N = (0.12, 0, 0) # NACA intro
for x in range(s_width):
x = float(x)/s_width
# Airfoil (U: Upper, L: Lower)
xL = xU = 20 + x * s_width
yU = height/2 - thickness(x, N) * s_width
yL = height/2 + thickness(x, N) * s_width
buffer.line((xU, yU, xL, yL), outline=color2, width=2)
buffer.point((xL, yL), outline=color3, width=2)
draw()
def tab_1():
"""NACA Plot tab"""
if not airfoil:
buffer.clear()
position = (10, 30)
color1 = (0, 0, 100) # Text
color2 = (0, 0, 0) # Axes
color3 = (80, 80, 80) # Units
color4 = (0, 0, 200) # Function
buffer.text(position, u'Please set a NACA from menu', fill=color1)
# Draws an axes system (origin in [w/3, h/2])
for k in range(30):
xA = k * width/30
yA = height/2
xO = width/3
yO = k * (height-50)/30
buffer.line((xA, yA-2, xA, yA+3), outline=color3)
buffer.line((xO-2, 50+yO, xO+3, 50+yO), outline=color3)
buffer.line((width/3, 50, width/3, height), outline=color2)
buffer.line((0, height/2, width, height/2), outline=color2)
# Draws a function
for t in range(900):
t = float(t)
x = t * cos(t*pi/180) / 15
y = t * sin(t*pi/180) / 15
buffer.point((width/3 + x, height/2 + y), outline=color4, width=2)
draw()
return
else:
NACA_plot()
def tab_2():
"""Dimensionless goup form"""
buffer.clear()
position = (10, 30)
color1 = (0, 0, 100)
color2 = (200, 0, 0)
buffer.text(position, u'Select a group from menu', fill=color1)
x0 = width / 2
y0 = height / 1.5
l = 15
for i in range(400):
x0 += randint(-l, l)
y0 += randint(-l, l)
if y0 < 50: y0 = 50 if y0 > height: y0 = height
if x0 < 0: x0 = 0 if x0 > width: x0 = width
buffer.point((x0, y0), outline=color2, width=2)
draw()
def tab_3():
"""Shows a form to calculate ISA parameters"""
if altitude == None:
buffer.clear()
position = (10, 30)
color = (0, 0, 100)
buffer.text(position, u'Please set altitude from menu', fill=color)
font = (u'Nokia Hindi TitleSmBd S6', 30)
isa_text = [u'International', u'Standard', u'Atmosphere']
buffer.text((10, 70), isa_text[0], font=font, fill=color)
buffer.text((30, 100), isa_text[1], font=font, fill=color)
buffer.text((50, 130), isa_text[2], font=font, fill=color)
draw()
return
temp, press, dens = ISA(altitude)
fields = [(u'Temperature [K]', 'text', u'%.3f' % temp),
(u'Temperature [\u00B0C]', 'text', u'%.3f' % (temp - 273.15)),
(u'Pressure [Pa]', 'text', u'%.3f' % press),
(u'Density [kg/m^3]', 'text', u'%.3f' % dens)]
flag = appuifw.FFormDoubleSpaced
form = appuifw.Form(fields, flag)
form.execute()
def set_tab(index):
"""Set tab function"""
if index == 0: # Starting
tab_0()
appuifw.app.menu = menu_0
elif index == 1: # NACA Plot
tab_1()
appuifw.app.menu = menu_1
elif index == 2: # Dim.less goup
tab_2()
appuifw.app.menu = menu_2
elif index == 3: # ISA
tab_3()
appuifw.app.menu = menu_3
tabs = [u'Intro', u'Plot', u'Group', u'ISA']
appuifw.app.set_tabs(tabs, set_tab)
# Starting menu
menu_0 = [(u'Rotate screen', rotate_screen),
(u'About', lambda: appuifw.note(u'Created by Ale152', 'info')),
(u'Quit', quit)]
# NACA Plot menu
menu_1 = [(u'Set NACA', NACA_set),
(u'Plot', NACA_plot),
(u'Export IMG', NACA_export),
(u'Rotate screen', rotate_screen),
(u'Quit', quit)]
# Dim.less group menu
menu_2 = [(u'Reynolds', Reynolds),
(u'Mach', Mach),
(u'Froude', Froude),
(u'Rotate screen', rotate_screen),
(u'Quit', quit)]
# ISA menu
menu_3 = [(u'Set altitude', ((u'Meters', lambda: set_altitude(um='m')),
(u'Feet', lambda: set_altitude(um='ft')))),
(u'Rotate screen', rotate_screen),
(u'Quit', quit)]
set_tab(0)
app_lock = e32.Ao_lock()
app_lock.wait()






