Skip to content

Commit 0b0e9b4

Browse files
authored
Merge pull request #3 from seanmcleod70/AirspeedTests
Add tests for airspeed conversion blocks
2 parents 26c9abb + 2d8b768 commit 0b0e9b4

3 files changed

Lines changed: 274 additions & 9 deletions

File tree

src/pathsim_flight/utils/airspeed_conversions.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _eval(self, cas, altitude):
9595

9696
mach = CAStoMach()._eval(cas, altitude)
9797
ISA = ISAtmosphere()
98-
_, _, _, speed_of_sound = ISA.state(altitude)
98+
_, _, _, speed_of_sound = ISA._eval(altitude)
9999
return mach * speed_of_sound
100100

101101

@@ -120,7 +120,7 @@ def _eval(self, tas, altitude):
120120
"""Assume m/s for input and output velocities and m for altitude."""
121121

122122
ISA = ISAtmosphere()
123-
pressure, _, _, speed_of_sound = ISA.state(altitude)
123+
pressure, _, _, speed_of_sound = ISA._eval(altitude)
124124

125125
mach = tas / speed_of_sound
126126
qc = pressure * ( math.pow(1 + 0.2*mach**2, 7/2) - 1)
@@ -148,8 +148,8 @@ def __init__(self):
148148
def _eval(self, cas, altitude):
149149
"""Assume m/s for input and output velocities and m for altitude."""
150150
ISA = ISAtmosphere()
151-
_, density, _, _ = ISA.state(altitude)
152-
_, rho0, _, _ = ISA.state(0) # Standard sea level density
151+
_, density, _, _ = ISA._eval(altitude)
152+
_, rho0, _, _ = ISA._eval(0) # Standard sea level density
153153
eas = CAStoTAS()._eval(cas, altitude) * math.sqrt(density / rho0)
154154
return eas
155155

@@ -174,8 +174,8 @@ def __init__(self):
174174
def _eval(self, eas, altitude):
175175
"""Assume m/s for input and output velocities and m for altitude."""
176176
ISA = ISAtmosphere()
177-
_, density, _, _ = ISA.state(altitude)
178-
_, rho0, _, _ = ISA.state(0) # Standard sea level density
177+
_, density, _, _ = ISA._eval(altitude)
178+
_, rho0, _, _ = ISA._eval(0) # Standard sea level density
179179
tas = eas * math.sqrt(rho0 / density)
180180
return tas
181181

@@ -197,9 +197,9 @@ class MachtoCAS(Function):
197197
def __init__(self):
198198
super().__init__(func=self._eval)
199199

200-
def _eval(mach, altitude):
200+
def _eval(self, mach, altitude):
201201
"""Assume m for altitude."""
202202
ISA = ISAtmosphere()
203-
_, _, _, speed_of_sound = ISA.state(altitude)
204-
return TAStoCAS()._eval(mach * speed_of_sound)
203+
_, _, _, speed_of_sound = ISA._eval(altitude)
204+
return TAStoCAS()._eval(mach * speed_of_sound, altitude)
205205

tests/utils/__init__.py

Whitespace-only changes.
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
########################################################################################
2+
##
3+
## TESTS FOR
4+
## 'utils.airspeed_conversions.py'
5+
##
6+
########################################################################################
7+
8+
# IMPORTS ==============================================================================
9+
10+
import unittest
11+
from pathsim_flight.utils.airspeed_conversions import (
12+
CAStoMach,
13+
CAStoTAS,
14+
TAStoCAS,
15+
CAStoEAS,
16+
EAStoTAS,
17+
MachtoCAS,
18+
)
19+
from pathsim_flight.atmosphere.international_standard_atmosphere import ISAtmosphere
20+
21+
22+
# TESTS ================================================================================
23+
24+
class TestCAStoMach(unittest.TestCase):
25+
"""Test the CAStoMach block."""
26+
27+
def test_port_labels(self):
28+
"""Test port label definitions."""
29+
self.assertEqual(CAStoMach.input_port_labels["cas"], 0)
30+
self.assertEqual(CAStoMach.input_port_labels["altitude"], 1)
31+
self.assertEqual(CAStoMach.output_port_labels["mach"], 0)
32+
33+
def test_zero_cas_returns_zero_mach(self):
34+
mach = CAStoMach()._eval(0.0, 0.0)
35+
self.assertAlmostEqual(mach, 0.0, places=9)
36+
37+
def test_monotonic_increase_and_supersonic_branch(self):
38+
a0 = ISAtmosphere.StdSL_speed_of_sound
39+
alt = 0.0
40+
cas_values = (1.0, 50.0, 150.0, 300.0, a0 + 1.0)
41+
mach_values = [CAStoMach()._eval(cas, alt) for cas in cas_values]
42+
43+
# monotonic increase
44+
for earlier, later in zip(mach_values, mach_values[1:]):
45+
self.assertLess(earlier, later)
46+
47+
# ensure supersonic branch yields mach > 1 for CAS slightly above speed of sound
48+
self.assertGreater(mach_values[-1], 1.0)
49+
50+
class TestCAStoTAS(unittest.TestCase):
51+
"""Test the CAStoTAS block."""
52+
53+
def test_port_labels(self):
54+
"""Test port label definitions."""
55+
self.assertEqual(CAStoTAS.input_port_labels["cas"], 0)
56+
self.assertEqual(CAStoTAS.input_port_labels["altitude"], 1)
57+
self.assertEqual(CAStoTAS.output_port_labels["tas"], 0)
58+
59+
def test_tas_equals_mach_times_speed_of_sound(self):
60+
cas = 120.0
61+
altitude = 5000.0
62+
63+
tas = CAStoTAS()._eval(cas, altitude)
64+
65+
mach = CAStoMach()._eval(cas, altitude)
66+
_, _, _, speed_of_sound = ISAtmosphere()._eval(altitude)
67+
68+
self.assertAlmostEqual(tas, mach * speed_of_sound, places=6)
69+
70+
def test_roundtrip_with_tastoCAS(self):
71+
for alt in (0.0, 5000.0, 11000.0):
72+
for cas in (1.0, 30.0, 120.0, 180.0):
73+
tas = CAStoTAS()._eval(cas, alt)
74+
cas_back = TAStoCAS()._eval(tas, alt)
75+
# numerical tolerance can be modest due to iterative formulas
76+
self.assertAlmostEqual(cas_back, cas, places=2)
77+
78+
class TestTAStoCAS(unittest.TestCase):
79+
"""Test the TAStoCAS block."""
80+
81+
def test_port_labels(self):
82+
self.assertEqual(TAStoCAS.input_port_labels["tas"], 0)
83+
self.assertEqual(TAStoCAS.input_port_labels["altitude"], 1)
84+
self.assertEqual(TAStoCAS.output_port_labels["cas"], 0)
85+
86+
def test_tas_to_cas_small_speed(self):
87+
# For small TAS at sea level CAS ~= TAS
88+
for tas in (0.0, 1.0, 5.0, 20.0):
89+
cas = TAStoCAS()._eval(tas, 0.0)
90+
self.assertAlmostEqual(cas, tas, places=4)
91+
92+
def test_tas_to_cas_inverse_of_cas_to_tas(self):
93+
# Ensure TAStoCAS(CAStoTAS(cas)) ~= cas
94+
for alt in (0.0, 3000.0, 10000.0):
95+
for cas in (5.0, 50.0, 150.0):
96+
tas = CAStoTAS()._eval(cas, alt)
97+
cas_back = TAStoCAS()._eval(tas, alt)
98+
self.assertAlmostEqual(cas_back, cas, places=3)
99+
100+
class TestCAStoEAS(unittest.TestCase):
101+
"""Test the CAStoEAS block."""
102+
103+
def test_port_labels(self):
104+
"""Test port label definitions."""
105+
self.assertEqual(CAStoEAS.input_port_labels["cas"], 0)
106+
self.assertEqual(CAStoEAS.input_port_labels["altitude"], 1)
107+
self.assertEqual(CAStoEAS.output_port_labels["eas"], 0)
108+
109+
def test_spot_speeds(self):
110+
"""Test CAS to EAS conversion at some spot speeds and altitudes."""
111+
112+
# References from - https://aerospaceweb.org/design/scripts/atmosphere/
113+
references = [
114+
[0, 0, 0],
115+
[100, 0, 100],
116+
[200, 0, 200],
117+
[300, 0, 300],
118+
[0 , 1000, 0],
119+
[100, 1000, 99.9],
120+
[200, 1000, 199.0],
121+
[300, 1000, 297.1],
122+
[0, 5000, 0],
123+
[100, 5000, 99.1],
124+
[200, 5000, 193.7],
125+
[300, 5000, 282.9],
126+
[0, 10000, 0],
127+
[100, 10000, 97.2],
128+
[200, 10000, 182.6],
129+
[300, 10000, 265.7]
130+
]
131+
132+
cas_to_eas = CAStoEAS()
133+
134+
for ref in references:
135+
cas_to_eas.inputs[0] = ref[0] # cas
136+
cas_to_eas.inputs[1] = ref[1] # altitude
137+
cas_to_eas.update(None)
138+
139+
self.assertAlmostEqual(cas_to_eas.outputs[0], ref[2], places=1) # eas
140+
141+
class TestEAStoTAS(unittest.TestCase):
142+
"""Test the EAStoTAS block."""
143+
144+
def test_port_labels(self):
145+
"""Test port label definitions."""
146+
self.assertEqual(EAStoTAS.input_port_labels["eas"], 0)
147+
self.assertEqual(EAStoTAS.input_port_labels["altitude"], 1)
148+
self.assertEqual(EAStoTAS.output_port_labels["tas"], 0)
149+
150+
def test_inverse_relationship_with_cas_to_eas(self):
151+
# EAStoTAS(CAStoEAS(cas, h), h) == CAStoTAS(cas, h)
152+
for alt in (0.0, 2000.0, 10000.0):
153+
for cas in (10.0, 50.0, 150.0):
154+
eas = CAStoEAS()._eval(cas, alt)
155+
tas_from_eas = EAStoTAS()._eval(eas, alt)
156+
tas_from_cas = CAStoTAS()._eval(cas, alt)
157+
self.assertAlmostEqual(tas_from_eas, tas_from_cas, places=6)
158+
159+
class TestMachtoCAS(unittest.TestCase):
160+
"""Test the MachtoCAS block."""
161+
162+
def test_port_labels(self):
163+
"""Test port label definitions."""
164+
self.assertEqual(MachtoCAS.input_port_labels["mach"], 0)
165+
self.assertEqual(MachtoCAS.input_port_labels["altitude"], 1)
166+
self.assertEqual(MachtoCAS.output_port_labels["cas"], 0)
167+
168+
def test_spot_machs(self):
169+
"""Test Mach to CAS conversion at some spot Mach numbers and altitudes."""
170+
171+
# References from - https://aerospaceweb.org/design/scripts/atmosphere/
172+
references = [
173+
[0.5, 0, 170.1],
174+
[1.2, 0, 408.4],
175+
[0.5, 5000, 126.0],
176+
[1.2, 5000, 318.6], # 317.0
177+
[0.5, 10000, 89.0],
178+
[1.2, 10000, 234.2] # 232.9
179+
]
180+
181+
mach_to_cas = MachtoCAS()
182+
183+
for ref in references:
184+
mach_to_cas.inputs[0] = ref[0] # mach
185+
mach_to_cas.inputs[1] = ref[1] # altitude
186+
mach_to_cas.update(None)
187+
self.assertAlmostEqual(mach_to_cas.outputs[0], ref[2], places=1)
188+
189+
190+
class TestGeneralAirspeedConversions(unittest.TestCase):
191+
"""Test general properties of the airspeed conversion blocks."""
192+
193+
def setUp(self):
194+
self.isa = ISAtmosphere()
195+
self.rho0 = self.isa._eval(0)[1]
196+
197+
def test_cas_to_eas_at_sea_level_equals_cas_to_tas(self):
198+
# At sea level density == rho0 so EAS == TAS
199+
for cas in (0.0, 5.0, 50.0, 150.0, 250.0):
200+
eas = CAStoEAS()._eval(cas, 0)
201+
tas = CAStoTAS()._eval(cas, 0)
202+
self.assertAlmostEqual(eas, tas, places=9,
203+
msg=f"CAS={cas}: CAStoEAS != CAStoTAS at sea level")
204+
205+
def test_eas_to_tas_inverse_of_cas_to_eas(self):
206+
# EAStoTAS(CAStoEAS(cas, h), h) == CAStoTAS(cas, h)
207+
for alt in (0.0, 2000.0, 10000.0):
208+
for cas in (20.0, 100.0, 200.0):
209+
eas = CAStoEAS()._eval(cas, alt)
210+
tas_from_eas = EAStoTAS()._eval(eas, alt)
211+
tas_from_cas = CAStoTAS()._eval(cas, alt)
212+
self.assertAlmostEqual(tas_from_eas, tas_from_cas, places=6,
213+
msg=f"alt={alt}, cas={cas}: EAStoTAS(CAStoEAS(.)) != CAStoTAS(.)")
214+
215+
def test_tas_to_cas_roundtrip(self):
216+
# CAStoTAS then TAStoCAS should return approximately the original CAS
217+
for alt in (0.0, 5000.0, 11000.0):
218+
for cas in (1.0, 30.0, 120.0, 180.0):
219+
tas = CAStoTAS()._eval(cas, alt)
220+
cas_back = TAStoCAS()._eval(tas, alt)
221+
# Allow small numerical differences
222+
self.assertAlmostEqual(cas_back, cas, places=2,
223+
msg=f"alt={alt}, cas={cas}: roundtrip CAS->TAS->CAS mismatch")
224+
225+
def test_cas_to_mach_monotonic_increasing(self):
226+
# Mach should increase with increasing calibrated airspeed
227+
alt = 0.0
228+
cas_values = (1.0, 50.0, 150.0, 300.0)
229+
mach_values = [CAStoMach()._eval(cas, alt) for cas in cas_values]
230+
for earlier, later in zip(mach_values, mach_values[1:]):
231+
self.assertLess(earlier, later, msg="Mach value did not increase with CAS")
232+
233+
def test_eas_tas_inverse_relationship(self):
234+
# EAStoTAS(CAStoEAS(cas, h), h) == CAStoTAS(cas, h)
235+
for alt in (0.0, 2000.0, 10000.0):
236+
for cas in (10.0, 50.0, 150.0):
237+
# CAS -> EAS
238+
cas_to_eas = CAStoEAS()
239+
cas_to_eas.inputs[0] = cas
240+
cas_to_eas.inputs[1] = alt
241+
cas_to_eas.update(None)
242+
eas = cas_to_eas.outputs[0]
243+
244+
# EAS -> TAS
245+
eas_to_tas = EAStoTAS()
246+
eas_to_tas.inputs[0] = eas
247+
eas_to_tas.inputs[1] = alt
248+
eas_to_tas.update(None)
249+
tas_from_eas = eas_to_tas.outputs[0]
250+
251+
# CAS -> TAS
252+
cas_to_tas = CAStoTAS()
253+
cas_to_tas.inputs[0] = cas
254+
cas_to_tas.inputs[1] = alt
255+
cas_to_tas.update(None)
256+
tas_from_cas = cas_to_tas.outputs[0]
257+
258+
self.assertAlmostEqual(tas_from_eas, tas_from_cas, places=6,
259+
msg=f"alt={alt}, cas={cas}: EAStoTAS(CAStoEAS(.)) != CAStoTAS(.)")
260+
261+
262+
# RUN TESTS LOCALLY ====================================================================
263+
264+
if __name__ == '__main__':
265+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)