diff --git a/tests/acceptance/test_prometheus_rocket.py b/tests/acceptance/test_prometheus_rocket.py new file mode 100644 index 000000000..e8392d2b6 --- /dev/null +++ b/tests/acceptance/test_prometheus_rocket.py @@ -0,0 +1,70 @@ +from rocketpy import Flight +from rocketpy.simulation.flight_data_importer import FlightDataImporter + + +def test_prometheus_rocket_data_asserts_acceptance( + environment_spaceport_america_2023, prometheus_rocket +): + """Tests the Prometheus rocket flight data against acceptance criteria. + + This function simulates a rocket flight using the given environment and + rocket parameters, then compares the simulated apogee with real flight data + to ensure the relative error is within acceptable thresholds. + + Parameters + ---------- + environment_spaceport_america_2023 : Environment + An environment configuration for Spaceport America in 2023. + prometheus_rocket : Rocket + The Prometheus rocket configuration. + + Raises + ------ + AssertionError + If the relative error between the simulated apogee and the real apogee + exceeds the threshold. + """ + # Define relative error threshold (defined manually based on data) + apogee_threshold = 7.5 / 100 + + # Simulate the flight + test_flight = Flight( + rocket=prometheus_rocket, + environment=environment_spaceport_america_2023, + inclination=80, + heading=75, + rail_length=5.18, + ) + + # Read the flight data + columns_map = { + "time": "time", + "altitude": "z", + "height": "altitude", + "acceleration": "acceleration", + "pressure": "pressure", + "accel_x": "ax", + "accel_y": "ay", + "accel_z": "az", + "latitude": "latitude", + "longitude": "longitude", + } + + altimeter_data = FlightDataImporter( + name="Telemetry Mega", + paths="data/prometheus/2022-06-24-serial-6583-flight-0003-TeleMega.csv", + columns_map=columns_map, + units=None, + interpolation="linear", + extrapolation="zero", + delimiter=",", + encoding="utf-8", + ) + + # Calculate errors and assert values + real_apogee = altimeter_data.altitude.max + rocketpy_apogee = test_flight.apogee - test_flight.env.elevation + a_error = abs(real_apogee - rocketpy_apogee) + r_error = a_error / real_apogee + + assert r_error < apogee_threshold, f"Apogee relative error is {r_error*100:.2f}%" diff --git a/tests/fixtures/environment/environment_fixtures.py b/tests/fixtures/environment/environment_fixtures.py index 851be3203..312401d6c 100644 --- a/tests/fixtures/environment/environment_fixtures.py +++ b/tests/fixtures/environment/environment_fixtures.py @@ -71,3 +71,30 @@ def env_analysis(): ) return env_analysis + + +@pytest.fixture +def environment_spaceport_america_2023(): + """Creates an Environment object for Spaceport America with a 2023 launch + conditions. + + Returns + ------- + rocketpy.Environment + Environment object configured for Spaceport America in 2023. + """ + env = Environment( + latitude=32.939377, + longitude=-106.911986, + elevation=1401, + ) + env.set_date(date=(2023, 6, 24, 9), timezone="America/Denver") + + env.set_atmospheric_model( + type="Reanalysis", + file="data/weather/spaceport_america_pressure_levels_2023_hourly.nc", + dictionary="ECMWF", + ) + + env.max_expected_height = 6000 + return env diff --git a/tests/fixtures/motor/generic_motor_fixtures.py b/tests/fixtures/motor/generic_motor_fixtures.py index 925716cb5..24a484bd1 100644 --- a/tests/fixtures/motor/generic_motor_fixtures.py +++ b/tests/fixtures/motor/generic_motor_fixtures.py @@ -28,3 +28,27 @@ def generic_motor(): ) return motor + + +@pytest.fixture +def generic_motor_cesaroni_M1520(): + """Defines a Cesaroni M1520 motor for the Prometheus rocket using the + GenericMotor class. + + Returns + ------- + GenericMotor + The Cesaroni M1520 motor for the Prometheus rocket. + """ + return GenericMotor( + # burn specs: https://www.thrustcurve.org/simfiles/5f4294d20002e900000006b1/ + thrust_source="data/motors/cesaroni/Cesaroni_7579M1520-P.eng", + burn_time=4.897, + propellant_initial_mass=3.737, + dry_mass=2.981, + # casing specs: Pro98 3G Gen2 casing + chamber_radius=0.064, + chamber_height=0.548, + chamber_position=0.274, + nozzle_radius=0.027, + ) diff --git a/tests/fixtures/rockets/rocket_fixtures.py b/tests/fixtures/rockets/rocket_fixtures.py index 3a1c82988..c89157b78 100644 --- a/tests/fixtures/rockets/rocket_fixtures.py +++ b/tests/fixtures/rockets/rocket_fixtures.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from rocketpy import Rocket @@ -276,3 +277,81 @@ def dimensionless_calisto(kg, m, dimensionless_cesaroni_m1670): ) example_rocket.add_motor(dimensionless_cesaroni_m1670, position=(-1.373) * m) return example_rocket + + +@pytest.fixture +def prometheus_rocket(generic_motor_cesaroni_M1520): + """Create a simple object of the Rocket class to be used in the tests. This + is the Prometheus rocket, a rocket documented in the Flight Examples section + of the RocketPy documentation. + + Parameters + ---------- + generic_motor_cesaroni_M1520 : GenericMotor + An object of the GenericMotor class. This is a pytest fixture too. + """ + + def prometheus_cd_at_ma(mach): + """Gives the drag coefficient of the rocket at a given mach number.""" + if mach <= 0.15: + return 0.422 + elif mach <= 0.45: + return 0.422 + (mach - 0.15) * (0.38 - 0.422) / (0.45 - 0.15) + elif mach <= 0.77: + return 0.38 + (mach - 0.45) * (0.32 - 0.38) / (0.77 - 0.45) + elif mach <= 0.82: + return 0.32 + (mach - 0.77) * (0.3 - 0.32) / (0.82 - 0.77) + elif mach <= 0.88: + return 0.3 + (mach - 0.82) * (0.3 - 0.3) / (0.88 - 0.82) + elif mach <= 0.94: + return 0.3 + (mach - 0.88) * (0.32 - 0.3) / (0.94 - 0.88) + elif mach <= 0.99: + return 0.32 + (mach - 0.94) * (0.37 - 0.32) / (0.99 - 0.94) + elif mach <= 1.04: + return 0.37 + (mach - 0.99) * (0.44 - 0.37) / (1.04 - 0.99) + elif mach <= 1.24: + return 0.44 + (mach - 1.04) * (0.43 - 0.44) / (1.24 - 1.04) + elif mach <= 1.33: + return 0.43 + (mach - 1.24) * (0.42 - 0.43) / (1.33 - 1.24) + elif mach <= 1.49: + return 0.42 + (mach - 1.33) * (0.39 - 0.42) / (1.49 - 1.33) + else: + return 0.39 + + prometheus = Rocket( + radius=0.06985, # 5.5" diameter circle + mass=13.93, + inertia=( + 4.87, + 4.87, + 0.05, + ), + power_off_drag=prometheus_cd_at_ma, + power_on_drag=lambda x: prometheus_cd_at_ma(x) * 1.02, # 5% increase in drag + center_of_mass_without_motor=0.9549, + coordinate_system_orientation="tail_to_nose", + ) + + prometheus.set_rail_buttons(0.69, 0.21, 60) + + prometheus.add_motor(motor=generic_motor_cesaroni_M1520, position=0) + nose_cone = prometheus.add_nose(length=0.742, kind="Von Karman", position=2.229) + fin_set = prometheus.add_trapezoidal_fins( + n=3, + span=0.13, + root_chord=0.268, + tip_chord=0.136, + position=0.273, + sweep_length=0.066, + ) + drogue_chute = prometheus.add_parachute( + "Drogue", + cd_s=1.6 * np.pi * 0.3048**2, # Cd = 1.6, D_chute = 24 in + trigger="apogee", + ) + main_chute = prometheus.add_parachute( + "Main", + cd_s=2.2 * np.pi * 0.9144**2, # Cd = 2.2, D_chute = 72 in + trigger=457.2, # 1500 ft + ) + return prometheus