Skip to content
Prev Previous commit
Next Next commit
TST: Improve tests for Flight Data Exporter
  • Loading branch information
Gui-FernandesBR committed Oct 5, 2025
commit 54a989dc3e6141e53d782ce5543595d811603f92
2 changes: 1 addition & 1 deletion rocketpy/simulation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .flight import Flight
from .flight_data_exporter import FlightDataExporter
from .flight_data_importer import FlightDataImporter
from .monte_carlo import MonteCarlo
from .multivariate_rejection_sampler import MultivariateRejectionSampler
from .flight_data_exporter import FlightDataExporter
2 changes: 1 addition & 1 deletion rocketpy/simulation/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
from ..prints.flight_prints import _FlightPrints
from ..tools import (
calculate_cubic_hermite_coefficients,
deprecated,
euler313_to_quaternions,
find_closest,
find_root_linear_interpolation,
find_roots_cubic_function,
quaternions_to_nutation,
quaternions_to_precession,
quaternions_to_spin,
deprecated,
)

ODE_SOLVER_MAP = {
Expand Down
1 change: 1 addition & 0 deletions rocketpy/simulation/flight_data_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import json

import numpy as np
import simplekml

Expand Down
125 changes: 0 additions & 125 deletions tests/integration/test_flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,131 +69,6 @@ def test_all_info_different_solvers(
assert test_flight.all_info() is None


class TestExportData:
"""Tests the export_data method of the Flight class."""

def test_basic_export(self, flight_calisto):
"""Tests basic export functionality"""
file_name = "test_export_data_1.csv"
flight_calisto.export_data(file_name)
self.validate_basic_export(flight_calisto, file_name)
os.remove(file_name)

def test_custom_export(self, flight_calisto):
"""Tests custom export functionality"""
file_name = "test_export_data_2.csv"
flight_calisto.export_data(
file_name,
"z",
"vz",
"e1",
"w3",
"angle_of_attack",
time_step=0.1,
)
self.validate_custom_export(flight_calisto, file_name)
os.remove(file_name)

def validate_basic_export(self, flight_calisto, file_name):
"""Validates the basic export file content"""
test_data = np.loadtxt(file_name, delimiter=",")
assert np.allclose(flight_calisto.x[:, 0], test_data[:, 0], atol=1e-5)
assert np.allclose(flight_calisto.x[:, 1], test_data[:, 1], atol=1e-5)
assert np.allclose(flight_calisto.y[:, 1], test_data[:, 2], atol=1e-5)
assert np.allclose(flight_calisto.z[:, 1], test_data[:, 3], atol=1e-5)
assert np.allclose(flight_calisto.vx[:, 1], test_data[:, 4], atol=1e-5)
assert np.allclose(flight_calisto.vy[:, 1], test_data[:, 5], atol=1e-5)
assert np.allclose(flight_calisto.vz[:, 1], test_data[:, 6], atol=1e-5)
assert np.allclose(flight_calisto.e0[:, 1], test_data[:, 7], atol=1e-5)
assert np.allclose(flight_calisto.e1[:, 1], test_data[:, 8], atol=1e-5)
assert np.allclose(flight_calisto.e2[:, 1], test_data[:, 9], atol=1e-5)
assert np.allclose(flight_calisto.e3[:, 1], test_data[:, 10], atol=1e-5)
assert np.allclose(flight_calisto.w1[:, 1], test_data[:, 11], atol=1e-5)
assert np.allclose(flight_calisto.w2[:, 1], test_data[:, 12], atol=1e-5)
assert np.allclose(flight_calisto.w3[:, 1], test_data[:, 13], atol=1e-5)

def validate_custom_export(self, flight_calisto, file_name):
"""Validates the custom export file content"""
test_data = np.loadtxt(file_name, delimiter=",")
time_points = np.arange(flight_calisto.t_initial, flight_calisto.t_final, 0.1)
assert np.allclose(time_points, test_data[:, 0], atol=1e-5)
assert np.allclose(flight_calisto.z(time_points), test_data[:, 1], atol=1e-5)
assert np.allclose(flight_calisto.vz(time_points), test_data[:, 2], atol=1e-5)
assert np.allclose(flight_calisto.e1(time_points), test_data[:, 3], atol=1e-5)
assert np.allclose(flight_calisto.w3(time_points), test_data[:, 4], atol=1e-5)
assert np.allclose(
flight_calisto.angle_of_attack(time_points), test_data[:, 5], atol=1e-5
)


def test_export_kml(flight_calisto_robust):
"""Tests weather the method Flight.export_kml is working as intended.

Parameters:
-----------
flight_calisto_robust : rocketpy.Flight
Flight object to be tested. See the conftest.py file for more info
regarding this pytest fixture.
"""

test_flight = flight_calisto_robust

# Basic export
test_flight.export_kml(
"test_export_data_1.kml", time_step=None, extrude=True, altitude_mode="absolute"
)

# Load exported files and fixtures and compare them
with open("test_export_data_1.kml", "r") as test_1:
for row in test_1:
if row[:29] == " <coordinates>":
r = row[29:-15]
r = r.split(",")
for i, j in enumerate(r):
r[i] = j.split(" ")
lon, lat, z, coords = [], [], [], []
for i in r:
for j in i:
coords.append(j)
for i in range(0, len(coords), 3):
lon.append(float(coords[i]))
lat.append(float(coords[i + 1]))
z.append(float(coords[i + 2]))
os.remove("test_export_data_1.kml")

assert np.allclose(test_flight.latitude[:, 1], lat, atol=1e-3)
assert np.allclose(test_flight.longitude[:, 1], lon, atol=1e-3)
assert np.allclose(test_flight.z[:, 1], z, atol=1e-3)


def test_export_pressures(flight_calisto_robust):
"""Tests if the method Flight.export_pressures is working as intended.

Parameters
----------
flight_calisto_robust : Flight
Flight object to be tested. See the conftest.py file for more info
regarding this pytest fixture.
"""
file_name = "pressures.csv"
time_step = 0.5
parachute = flight_calisto_robust.rocket.parachutes[0]

flight_calisto_robust.export_pressures(file_name, time_step)

with open(file_name, "r") as file:
contents = file.read()

expected_data = ""
for t in np.arange(0, flight_calisto_robust.t_final, time_step):
p_cl = parachute.clean_pressure_signal_function(t)
p_ns = parachute.noisy_pressure_signal_function(t)
expected_data += f"{t:f}, {p_cl:.5f}, {p_ns:.5f}\n"

assert contents == expected_data
os.remove(file_name)


@patch("matplotlib.pyplot.show")
def test_hybrid_motor_flight(mock_show, flight_calisto_hybrid_modded): # pylint: disable=unused-argument
"""Test the flight of a rocket with a hybrid motor. This test only validates
Expand Down
178 changes: 164 additions & 14 deletions tests/unit/test_flight_data_exporter.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
"""Unit tests for FlightDataExporter class.

This module tests the data export functionality of the FlightDataExporter class,
which exports flight simulation data to various formats (CSV, JSON, KML).
"""

import json

import numpy as np

from rocketpy.simulation import FlightDataExporter


def test_export_data_writes_csv_header(flight_calisto, tmp_path):
"""Expect: direct exporter writes a CSV with a header containing 'Time (s)'."""
out = tmp_path / "out.csv"
FlightDataExporter(flight_calisto).export_data(str(out))
text = out.read_text(encoding="utf-8")
assert "Time (s)" in text
def test_export_pressures_writes_csv_rows(flight_calisto_robust, tmp_path):
"""Test that export_pressures writes CSV rows with pressure data.

Validates that the exported file contains multiple data rows in CSV format
with 2-3 columns (time and pressure values).

def test_export_pressures_writes_rows(flight_calisto_robust, tmp_path):
"""Expect: direct exporter writes a pressure file with time-first CSV rows."""
out = tmp_path / "p.csv"
Parameters
----------
flight_calisto_robust : rocketpy.Flight
Flight object with parachutes configured.
tmp_path : pathlib.Path
Pytest fixture for temporary directories.
"""
out = tmp_path / "pressures.csv"
FlightDataExporter(flight_calisto_robust).export_pressures(str(out), time_step=0.2)
lines = out.read_text(encoding="utf-8").strip().splitlines()
assert len(lines) > 5
# basic CSV shape t, value
# Basic CSV shape "t, value" or "t, clean, noisy"
parts = lines[0].split(",")
assert len(parts) in (2, 3)


def test_export_sensor_data_writes_json_when_sensor_data_present(
flight_calisto, tmp_path, monkeypatch
):
"""Expect: exporter writes JSON mapping sensor.name -> data when sensor_data is present."""
def test_export_sensor_data_writes_json(flight_calisto, tmp_path, monkeypatch):
"""Test that export_sensor_data writes JSON with sensor data.

Validates that sensor data is exported as JSON with sensor names as keys
and measurement arrays as values.

Parameters
----------
flight_calisto : rocketpy.Flight
Flight object to be tested.
tmp_path : pathlib.Path
Pytest fixture for temporary directories.
monkeypatch : pytest.MonkeyPatch
Pytest fixture for modifying attributes.
"""

class DummySensor:
def __init__(self, name):
Expand All @@ -40,3 +63,130 @@ def __init__(self, name):

data = json.loads(out.read_text(encoding="utf-8"))
assert data["DummySensor"] == [1.0, 2.0, 3.0]


def test_export_data_default_variables(flight_calisto, tmp_path):
"""Test export_data with default variables (full solution matrix).

Validates that all state variables are exported correctly when no specific
variables are requested: position (x, y, z), velocity (vx, vy, vz),
quaternions (e0, e1, e2, e3), and angular velocities (w1, w2, w3).

Parameters
----------
flight_calisto : rocketpy.Flight
Flight object to be tested.
tmp_path : pathlib.Path
Pytest fixture for temporary directories.
"""
file_name = tmp_path / "flight_data.csv"
FlightDataExporter(flight_calisto).export_data(str(file_name))

test_data = np.loadtxt(file_name, delimiter=",", skiprows=1)

# Verify time column
assert np.allclose(flight_calisto.x[:, 0], test_data[:, 0], atol=1e-5)

# Verify position
assert np.allclose(flight_calisto.x[:, 1], test_data[:, 1], atol=1e-5)
assert np.allclose(flight_calisto.y[:, 1], test_data[:, 2], atol=1e-5)
assert np.allclose(flight_calisto.z[:, 1], test_data[:, 3], atol=1e-5)

# Verify velocity
assert np.allclose(flight_calisto.vx[:, 1], test_data[:, 4], atol=1e-5)
assert np.allclose(flight_calisto.vy[:, 1], test_data[:, 5], atol=1e-5)
assert np.allclose(flight_calisto.vz[:, 1], test_data[:, 6], atol=1e-5)

# Verify quaternions
assert np.allclose(flight_calisto.e0[:, 1], test_data[:, 7], atol=1e-5)
assert np.allclose(flight_calisto.e1[:, 1], test_data[:, 8], atol=1e-5)
assert np.allclose(flight_calisto.e2[:, 1], test_data[:, 9], atol=1e-5)
assert np.allclose(flight_calisto.e3[:, 1], test_data[:, 10], atol=1e-5)

# Verify angular velocities
assert np.allclose(flight_calisto.w1[:, 1], test_data[:, 11], atol=1e-5)
assert np.allclose(flight_calisto.w2[:, 1], test_data[:, 12], atol=1e-5)
assert np.allclose(flight_calisto.w3[:, 1], test_data[:, 13], atol=1e-5)


def test_export_data_custom_variables_and_time_step(flight_calisto, tmp_path):
"""Test export_data with custom variables and time step.

Validates that specific variables can be exported with custom time intervals,
including derived quantities like angle of attack.

Parameters
----------
flight_calisto : rocketpy.Flight
Flight object to be tested.
tmp_path : pathlib.Path
Pytest fixture for temporary directories.
"""
file_name = tmp_path / "custom_flight_data.csv"
time_step = 0.1

FlightDataExporter(flight_calisto).export_data(
str(file_name),
"z",
"vz",
"e1",
"w3",
"angle_of_attack",
time_step=time_step,
)

test_data = np.loadtxt(file_name, delimiter=",", skiprows=1)
time_points = np.arange(flight_calisto.t_initial, flight_calisto.t_final, time_step)

# Verify time column
assert np.allclose(time_points, test_data[:, 0], atol=1e-5)

# Verify custom variables
assert np.allclose(flight_calisto.z(time_points), test_data[:, 1], atol=1e-5)
assert np.allclose(flight_calisto.vz(time_points), test_data[:, 2], atol=1e-5)
assert np.allclose(flight_calisto.e1(time_points), test_data[:, 3], atol=1e-5)
assert np.allclose(flight_calisto.w3(time_points), test_data[:, 4], atol=1e-5)
assert np.allclose(
flight_calisto.angle_of_attack(time_points), test_data[:, 5], atol=1e-5
)


def test_export_kml_trajectory(flight_calisto_robust, tmp_path):
"""Test export_kml creates valid KML file with trajectory coordinates.

Validates that the KML export contains correct latitude, longitude, and
altitude coordinates for the flight trajectory in absolute altitude mode.

Parameters
----------
flight_calisto_robust : rocketpy.Flight
Flight object to be tested.
tmp_path : pathlib.Path
Pytest fixture for temporary directories.
"""
file_name = tmp_path / "trajectory.kml"
FlightDataExporter(flight_calisto_robust).export_kml(
str(file_name), time_step=None, extrude=True, altitude_mode="absolute"
)

# Parse KML coordinates
with open(file_name, "r") as kml_file:
for row in kml_file:
if row.strip().startswith("<coordinates>"):
coords_str = (
row.strip()
.replace("<coordinates>", "")
.replace("</coordinates>", "")
)
coords_list = coords_str.strip().split(" ")

# Extract lon, lat, z from coordinates
parsed_coords = [c.split(",") for c in coords_list]
lon = [float(point[0]) for point in parsed_coords]
lat = [float(point[1]) for point in parsed_coords]
z = [float(point[2]) for point in parsed_coords]

# Verify coordinates match flight data
assert np.allclose(flight_calisto_robust.latitude[:, 1], lat, atol=1e-3)
assert np.allclose(flight_calisto_robust.longitude[:, 1], lon, atol=1e-3)
assert np.allclose(flight_calisto_robust.z[:, 1], z, atol=1e-3)
3 changes: 3 additions & 0 deletions tests/unit/test_flight_export_deprecation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from unittest.mock import patch

import pytest

# TODO: these tests should be deleted after the deprecated methods are removed


def test_export_data_deprecated_emits_warning_and_delegates(flight_calisto, tmp_path):
"""Expect: calling Flight.export_data emits DeprecationWarning and delegates to FlightDataExporter.export_data."""
Expand Down