Skip to content

Commit 529ff4d

Browse files
committed
Initial JSBSimWrapper, ISA and airspeed blocks
1 parent 12dcba2 commit 529ff4d

4 files changed

Lines changed: 411 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ ENV/
7171
# IDEs
7272
.vscode/
7373
.idea/
74+
.vs/
7475

7576
# OS generated files
7677
.DS_Store
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#########################################################################################
2+
##
3+
## JSBSim Wrapper Block
4+
##
5+
#########################################################################################
6+
7+
# IMPORTS ===============================================================================
8+
9+
from pathsim.blocks import Function
10+
from ISA import ISAtmosphere
11+
import math
12+
13+
# BLOCKS ================================================================================
14+
15+
class CAStoMach(Function):
16+
"""Convert calibrated airspeed (CAS) to Mach value.
17+
CAS in m/s altitude in m.
18+
"""
19+
20+
input_port_labels = {
21+
"cas": 0,
22+
"altitude": 1
23+
}
24+
25+
output_port_labels = {
26+
"mach": 0
27+
}
28+
29+
def __init__(self):
30+
super().__init__(func=self._eval)
31+
32+
def _eval(self, cas, altitude):
33+
"""Convert Calibrated airspeed to Mach value.
34+
35+
Assume m/s for cas and m for altitude.
36+
37+
Based on the formulas in the US Air Force Aircraft Performance Flight
38+
Testing Manual (AFFTC-TIH-99-01), in particular sections 4.6 to 4.8.
39+
40+
The subsonic and supersonic Mach number equations are used with the simple
41+
substitutions of (Vc/asl) for M and Psl for P. However, the condition for
42+
which the equations are used is no longer subsonic (M < 1) or supersonic
43+
(M > 1) but rather calibrated airspeed being less or greater than the
44+
speed of sound ( asl ), standard day, sea level (661.48 knots).
45+
"""
46+
ISA = ISAtmosphere()
47+
48+
pressure, _, _, _ = ISA._eval(altitude)
49+
50+
if cas < ISAtmosphere.StdSL_speed_of_sound:
51+
# Bernoulli's compressible equation (4.11)
52+
qc = ISAtmosphere.StdSL_pressure * (
53+
math.pow(1 + 0.2 * math.pow(cas / ISAtmosphere.StdSL_speed_of_sound, 2), 3.5) - 1
54+
)
55+
else:
56+
# Rayleigh's supersonic pitot equation (4.16)
57+
qc = ISAtmosphere.StdSL_pressure * (
58+
(
59+
(166.9215801 * math.pow(cas / ISAtmosphere.StdSL_speed_of_sound, 7))
60+
/ math.pow(7 * math.pow(cas / ISAtmosphere.StdSL_speed_of_sound, 2) - 1, 2.5)
61+
)
62+
- 1
63+
)
64+
65+
# Solving for M in equation (4.11), also used as initial condition for supersonic case
66+
mach = math.sqrt(5 * (math.pow(qc / pressure + 1, 2 / 7) - 1))
67+
68+
if mach > 1:
69+
# Iterate equation (4.22) since M appears on both sides of the equation
70+
for i in range(7):
71+
mach = 0.88128485 * math.sqrt((qc / pressure + 1) * math.pow(1 - 1 / (7 * mach * mach), 2.5))
72+
73+
return mach
74+
75+
76+
class CAStoTAS(Function):
77+
"""Convert calibrated airspeed (CAS) to true airspeed (TAS).
78+
CAS and TAS in m/s altitude in m.
79+
"""
80+
81+
input_port_labels = {
82+
"cas": 0,
83+
"altitude": 1
84+
}
85+
86+
output_port_labels = {
87+
"tas": 0
88+
}
89+
90+
def __init__(self):
91+
super().__init__(func=self._eval)
92+
93+
def _eval(self, cas, altitude):
94+
"""Assume m/s for input and output velocities and m for altitude."""
95+
96+
mach = CAStoMach()._eval(cas, altitude)
97+
ISA = ISAtmosphere()
98+
_, _, _, speed_of_sound = ISA.state(altitude)
99+
return mach * speed_of_sound
100+
101+
102+
class TAStoCAS(Function):
103+
"""Convert true airspeed (TAS) to calibrated airspeed (CAS).
104+
TAS and CAS in m/s altitude in m.
105+
"""
106+
107+
input_port_labels = {
108+
"tas": 0,
109+
"altitude": 1
110+
}
111+
112+
output_port_labels = {
113+
"cas": 0
114+
}
115+
116+
def __init__(self):
117+
super().__init__(func=self._eval)
118+
119+
def _eval(self, tas, altitude):
120+
"""Assume m/s for input and output velocities and m for altitude."""
121+
122+
ISA = ISAtmosphere()
123+
pressure, _, _, speed_of_sound = ISA.state(altitude)
124+
125+
mach = tas / speed_of_sound
126+
qc = pressure * ( math.pow(1 + 0.2*mach**2, 7/2) - 1)
127+
cas = ISA.StdSL_speed_of_sound * math.sqrt( 5 * ( math.pow(qc/ISA.StdSL_pressure + 1, 2/7) - 1) )
128+
return cas
129+
130+
131+
class CAStoEAS(Function):
132+
"""Convert calibrated airspeed (CAS) to equivalent airspeed (EAS).
133+
CAS and EAS in m/s altitude in m.
134+
"""
135+
136+
input_port_labels = {
137+
"cas": 0,
138+
"altitude": 1
139+
}
140+
141+
output_port_labels = {
142+
"eas": 0
143+
}
144+
145+
def __init__(self):
146+
super().__init__(func=self._eval)
147+
148+
def _eval(self, cas, altitude):
149+
"""Assume m/s for input and output velocities and m for altitude."""
150+
ISA = ISAtmosphere()
151+
_, density, _, _ = ISA.state(altitude)
152+
_, rho0, _, _ = ISA.state(0) # Standard sea level density
153+
eas = CAStoTAS()._eval(cas, altitude) * math.sqrt(density / rho0)
154+
return eas
155+
156+
157+
class EAStoTAS(Function):
158+
"""Convert equivalent airspeed (EAS) to true airspeed (TAS).
159+
EAS and TAS in m/s altitude in m.
160+
"""
161+
162+
input_port_labels = {
163+
"eas": 0,
164+
"altitude": 1
165+
}
166+
167+
output_port_labels = {
168+
"tas": 0
169+
}
170+
171+
def __init__(self):
172+
super().__init__(func=self._eval)
173+
174+
def _eval(self, eas, altitude):
175+
"""Assume m/s for input and output velocities and m for altitude."""
176+
ISA = ISAtmosphere()
177+
_, density, _, _ = ISA.state(altitude)
178+
_, rho0, _, _ = ISA.state(0) # Standard sea level density
179+
tas = eas * math.sqrt(rho0 / density)
180+
return tas
181+
182+
183+
class MachtoCAS(Function):
184+
"""Convert Mach value to calibrated airspeed (CAS).
185+
CAS in m/s altitude in m.
186+
"""
187+
188+
input_port_labels = {
189+
"mach": 0,
190+
"altitude": 1
191+
}
192+
193+
output_port_labels = {
194+
"cas": 0
195+
}
196+
197+
def __init__(self):
198+
super().__init__(func=self._eval)
199+
200+
def _eval(mach, altitude):
201+
"""Assume m for altitude."""
202+
ISA = ISAtmosphere()
203+
_, _, _, speed_of_sound = ISA.state(altitude)
204+
return TAStoCAS()._eval(mach * speed_of_sound)
205+

src/pathsim_flight/ISA.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#########################################################################################
2+
##
3+
## International Standard Atmosphere Block
4+
##
5+
#########################################################################################
6+
7+
# IMPORTS ===============================================================================
8+
9+
from pathsim.blocks import Function
10+
import math
11+
from collections import namedtuple
12+
13+
# BLOCKS ================================================================================
14+
15+
class ISAtmosphere(Function):
16+
"""International Standard Atmosphere.
17+
18+
For a given geometric altitude and temperature deviation from standard day,
19+
compute the pressure, density, temperature, and speed of sound.
20+
21+
See - https://seanmcleod70.github.io/FlightDynamicsCalcs/InternationalStandardAtmosphere.html
22+
"""
23+
24+
input_port_labels = {
25+
"altitude": 0,
26+
"temp_deviation": 1
27+
}
28+
29+
output_port_labels = {
30+
"pressure": 0,
31+
"density": 1,
32+
"temperature": 2,
33+
"speed_of_sound": 3
34+
}
35+
36+
def __init__(self):
37+
super().__init__(func=self._eval)
38+
39+
# Constants
40+
R = 287.0528 # Specific gas constant
41+
g0 = 9.80665 # Gravitational acceleration
42+
gamma = 1.4 # Air specific heat ratio
43+
r0 = 6356766 # Earth radius
44+
45+
StdSL_pressure = 101325 # Pa
46+
StdSL_speed_of_sound = 340.294 # m/s
47+
48+
# Atmosphere bands
49+
AtmosphereBand = namedtuple('AtmosphereBand', ['start_alt', 'end_alt',
50+
'base_temperature', 'base_pressure',
51+
'lapse_rate'])
52+
53+
atmosphere_bands = [
54+
AtmosphereBand(0, 11000, 288.15, 101325, -0.0065),
55+
AtmosphereBand(11000, 20000, 216.65, 22632, 0.0),
56+
AtmosphereBand(20000, 32000, 216.65, 5474.9, 0.001),
57+
AtmosphereBand(32000, 47000, 228.65, 868.02, 0.0028),
58+
AtmosphereBand(47000, 51000, 270.65, 110.91, 0.0),
59+
AtmosphereBand(51000, 71000, 270.65, 66.939, -0.0028),
60+
AtmosphereBand(71000, 84852, 214.65, 3.9564, -0.002),
61+
]
62+
63+
def _eval(self, geometric_altitude, delta_temp=0):
64+
geopot_altitude = self.geopotential_altitude(geometric_altitude)
65+
band_data = self.get_atmosphere_band(geopot_altitude)
66+
67+
dh = geopot_altitude - band_data.start_alt
68+
lapse_rate = band_data.lapse_rate
69+
70+
temp = 0
71+
pressure = 0
72+
density = 0
73+
speed_of_sound = 0
74+
75+
if lapse_rate != 0.0:
76+
temp = band_data.base_temperature + lapse_rate * dh
77+
pressure = band_data.base_pressure * math.pow(temp/band_data.base_temperature, -self.g0/(lapse_rate * self.R))
78+
else:
79+
temp = band_data.base_temperature
80+
pressure = band_data.base_pressure * math.exp((-self.g0 * dh)/(self.R * temp))
81+
82+
density = pressure/(self.R * (temp + delta_temp))
83+
speed_of_sound = math.sqrt(self.gamma * self.R * (temp + delta_temp))
84+
85+
return (pressure, density, temp + delta_temp, speed_of_sound)
86+
87+
def geopotential_altitude(self, geometric_altitude):
88+
return (geometric_altitude * self.r0)/(self.r0 + geometric_altitude)
89+
90+
def geometric_altitude(self, geopotential_altitude):
91+
return (self.r0 * geopotential_altitude)/(self.r0 - geopotential_altitude)
92+
93+
def get_atmosphere_band(self, geopot_altitude):
94+
for band in self.atmosphere_bands:
95+
if geopot_altitude >= band.start_alt and geopot_altitude <= band.end_alt:
96+
return band
97+
raise IndexError('Altitude out of range')
98+
99+

0 commit comments

Comments
 (0)