Skip to content

Commit 6bf70f3

Browse files
authored
ENH: Support for the RSE file format has been added to the library (#798)
* ENH: Support for the RSE file format has been added to the library. The import_rse method in the Abstract Motor class and the load_from_rse_file method in the GenericMotor class are now available. With this update, the library natively supports Rock Sim software data, eliminating the need for users to manually convert motor files. The implementation was based on the import_eng and load_from_eng_file methods, utilizing Python's standard XML library. * ENH: Adding tests to the methods of .rse file treatment. * ENH: fixing mistakes on the method and test file * MNT: Running ruff * MNT: Adding the PR to CHANGELOG.md
1 parent 220bb59 commit 6bf70f3

File tree

5 files changed

+272
-3
lines changed

5 files changed

+272
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Attention: The newest changes should be on top -->
3232

3333
### Added
3434

35+
- ENH: Support for the RSE file format has been added to the library [#798](https://github.com/RocketPy-Team/RocketPy/pull/798)
3536

3637
### Changed
3738

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<engine-database>
2+
<engine-list>
3+
<engine FDiv="10" FFix="1" FStep="-1." Isp="195.96" Itot="39.78" Type="reloadable" auto-calc-cg="1" auto-calc-mass="1"
4+
avgThrust="18.082" burn-time="2.2" cgDiv="10" cgFix="1" cgStep="-1." code="E18" delays="4,8" dia="24." exitDia="0." initWt="57."
5+
len="70." mDiv="10" mFix="1" mStep="-1." massFrac="36.32" mfg="Aerotech" peakThrust="31." propWt="20.7" tDiv="10" tFix="1"
6+
tStep="-1." throatDia="0.">
7+
<data>
8+
<eng-data cg="35." f="0." m="20.7" t="0."/>
9+
<eng-data cg="35." f="31." m="20.3774" t="0.04"/>
10+
<eng-data cg="35." f="30." m="14.6638" t="0.4"/>
11+
<eng-data cg="35." f="26.7" m="8.7629" t="0.8"/>
12+
<eng-data cg="35." f="19.1" m="3.99638" t="1.2"/>
13+
<eng-data cg="35." f="8." m="1.17602" t="1.6"/>
14+
<eng-data cg="35." f="2.2" m="0.11448" t="2."/>
15+
<eng-data cg="35." f="0." m="0." t="2.2"/>
16+
</data>
17+
</engine>
18+
</engine-list>
19+
</engine-database>

rocketpy/motors/motor.py

Lines changed: 220 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
import warnings
3+
import xml.etree.ElementTree as ET
34
from abc import ABC, abstractmethod
45
from functools import cached_property
56
from os import path
@@ -268,16 +269,22 @@ class Function. Thrust units are Newtons.
268269
self.dry_I_13 = inertia[4]
269270
self.dry_I_23 = inertia[5]
270271

271-
# Handle .eng file inputs
272+
# Handle .eng or .rse file inputs
272273
self.description_eng_file = None
274+
self.rse_motor_data = None
273275
if isinstance(thrust_source, str):
274276
if (
275277
path.exists(thrust_source)
276278
and path.splitext(path.basename(thrust_source))[1] == ".eng"
277279
):
278280
_, self.description_eng_file, points = Motor.import_eng(thrust_source)
279281
thrust_source = points
280-
282+
elif (
283+
path.exists(thrust_source)
284+
and path.splitext(path.basename(thrust_source))[1] == ".rse"
285+
):
286+
self.rse_motor_data, points = Motor.import_rse(thrust_source)
287+
thrust_source = points
281288
# Evaluate raw thrust source
282289
self.thrust_source = thrust_source
283290
self.thrust = Function(
@@ -381,6 +388,10 @@ def dry_mass(self, dry_mass):
381388
self._dry_mass = float(self.description_eng_file[-2]) - float(
382389
self.description_eng_file[-3]
383390
)
391+
elif self.rse_motor_data:
392+
self._dry_mass = float(
393+
self.rse_motor_data["description"]["total_mass"]
394+
) - float(self.rse_motor_data["description"]["propellant_mass"])
384395
else:
385396
raise ValueError("Dry mass must be specified.")
386397

@@ -990,6 +1001,83 @@ def clip_thrust(thrust, new_burn_time):
9901001
"zero",
9911002
)
9921003

1004+
@staticmethod
1005+
def import_rse(file_name):
1006+
"""
1007+
Reads motor data from a file and extracts comments, model, description, and data points.
1008+
1009+
Parameters
1010+
----------
1011+
file_path : str
1012+
Path to the motor data file.
1013+
1014+
Returns
1015+
-------
1016+
dict
1017+
A dictionary containing the extracted data:
1018+
- comments: List of comments in the file.
1019+
- model: Dictionary with manufacturer, code, and type of the motor.
1020+
- description: Dictionary with performance data (dimensions, weights, thrust, etc.).
1021+
- data_points: List of temporal data points (time, thrust, mass, cg).
1022+
tuple
1023+
A tuple representing the thrust curve (time, thrust).
1024+
"""
1025+
1026+
# Parse the XML file
1027+
tree = ET.parse(file_name)
1028+
root = tree.getroot()
1029+
1030+
# Extract comments
1031+
comments = []
1032+
for comment in root.iter():
1033+
if comment.tag.startswith("<!--"):
1034+
comments.append(comment.text.strip())
1035+
1036+
# Extract model data
1037+
engine = root.find(".//engine")
1038+
model = {
1039+
"manufacturer": engine.attrib.get("mfg"),
1040+
"code": engine.attrib.get("code"),
1041+
"type": engine.attrib.get("Type"),
1042+
}
1043+
1044+
# Extract description data
1045+
description = {
1046+
"diameter": float(engine.attrib.get("dia", 0)) / 1000,
1047+
"length": float(engine.attrib.get("len", 0)) / 1000,
1048+
"throat_diameter": float(engine.attrib.get("throatDia", 0)) / 1000,
1049+
"exit_diameter": float(engine.attrib.get("exitDia", 0)) / 1000,
1050+
"total_mass": float(engine.attrib.get("initWt", 0)) / 1000,
1051+
"propellant_mass": float(engine.attrib.get("propWt", 0)) / 1000,
1052+
"average_thrust": float(engine.attrib.get("avgThrust", 0)),
1053+
"peak_thrust": float(engine.attrib.get("peakThrust", 0)),
1054+
"total_impulse": float(engine.attrib.get("Itot", 0)),
1055+
"burn_time": float(engine.attrib.get("burn-time", 0)),
1056+
"isp": float(engine.attrib.get("Isp", 0)),
1057+
"mass_fraction": float(engine.attrib.get("massFrac", 0)) / 100,
1058+
}
1059+
1060+
# Extract data points
1061+
data_points = []
1062+
thrust_source = []
1063+
for eng_data in engine.find("data").findall("eng-data"):
1064+
time = float(eng_data.attrib.get("t", 0))
1065+
thrust = float(eng_data.attrib.get("f", 0))
1066+
mass = float(eng_data.attrib.get("m", 0))
1067+
cg = float(eng_data.attrib.get("cg", 0))
1068+
data_points.append({"time": time, "thrust": thrust, "mass": mass, "cg": cg})
1069+
thrust_source.append([time, thrust])
1070+
1071+
# Create the dictionary to return
1072+
rse_file_data = {
1073+
"comments": comments,
1074+
"model": model,
1075+
"description": description,
1076+
"data_points": data_points,
1077+
}
1078+
1079+
return rse_file_data, thrust_source
1080+
9931081
@staticmethod
9941082
def import_eng(file_name):
9951083
"""Read content from .eng file and process it, in order to return the
@@ -1566,6 +1654,136 @@ def load_from_eng_file(
15661654
coordinate_system_orientation=coordinate_system_orientation,
15671655
)
15681656

1657+
@staticmethod
1658+
def load_from_rse_file(
1659+
file_name,
1660+
nozzle_radius=None,
1661+
chamber_radius=None,
1662+
chamber_height=None,
1663+
chamber_position=0,
1664+
propellant_initial_mass=None,
1665+
dry_mass=None,
1666+
burn_time=None,
1667+
center_of_dry_mass_position=None,
1668+
dry_inertia=(0, 0, 0),
1669+
nozzle_position=0,
1670+
reshape_thrust_curve=False,
1671+
interpolation_method="linear",
1672+
coordinate_system_orientation="nozzle_to_combustion_chamber",
1673+
):
1674+
"""Loads motor data from a .rse file and processes it.
1675+
1676+
Parameters
1677+
----------
1678+
file_name : string
1679+
Name of the .eng file. E.g. 'test.eng'.
1680+
nozzle_radius : int, float
1681+
Motor's nozzle outlet radius in meters.
1682+
chamber_radius : int, float, optional
1683+
The radius of a overall cylindrical chamber of propellant in meters.
1684+
chamber_height : int, float, optional
1685+
The height of a overall cylindrical chamber of propellant in meters.
1686+
chamber_position : int, float, optional
1687+
The position, in meters, of the centroid (half height) of the motor's
1688+
overall cylindrical chamber of propellant with respect to the motor's
1689+
coordinate system.
1690+
propellant_initial_mass : int, float, optional
1691+
The initial mass of the propellant in the motor.
1692+
dry_mass : int, float, optional
1693+
Same as in Motor class. See the :class:`Motor <rocketpy.Motor>` docs
1694+
burn_time: float, tuple of float, optional
1695+
Motor's burn time.
1696+
If a float is given, the burn time is assumed to be between 0 and
1697+
the given float, in seconds.
1698+
If a tuple of float is given, the burn time is assumed to be between
1699+
the first and second elements of the tuple, in seconds.
1700+
If not specified, automatically sourced as the range between the
1701+
first and last-time step of the motor's thrust curve. This can only
1702+
be used if the motor's thrust is defined by a list of points, such
1703+
as a .csv file, a .eng file or a Function instance whose source is a
1704+
list.
1705+
center_of_dry_mass_position : int, float, optional
1706+
The position, in meters, of the motor's center of mass with respect
1707+
to the motor's coordinate system when it is devoid of propellant.
1708+
If not specified, automatically sourced as the chamber position.
1709+
dry_inertia : tuple, list
1710+
Tuple or list containing the motor's dry mass inertia tensor
1711+
nozzle_position : int, float, optional
1712+
Motor's nozzle outlet position in meters, in the motor's coordinate
1713+
system. Default is 0, in which case the origin of the
1714+
coordinate system is placed at the motor's nozzle outlet.
1715+
reshape_thrust_curve : boolean, tuple, optional
1716+
If False, the original thrust curve supplied is not altered. If a
1717+
tuple is given, whose first parameter is a new burn out time and
1718+
whose second parameter is a new total impulse in Ns, the thrust
1719+
curve is reshaped to match the new specifications. May be useful
1720+
for motors whose thrust curve shape is expected to remain similar
1721+
in case the impulse and burn time varies slightly. Default is
1722+
False. Note that the Motor burn_time parameter must include the new
1723+
reshaped burn time.
1724+
interpolation_method : string, optional
1725+
Method of interpolation to be used in case thrust curve is given
1726+
coordinate_system_orientation : string, optional
1727+
Orientation of the motor's coordinate system. The coordinate system
1728+
is defined by the motor's axis of symmetry. The origin of the
1729+
coordinate system may be placed anywhere along such axis, such as
1730+
at the nozzle area, and must be kept the same for all other
1731+
positions specified. Options are "nozzle_to_combustion_chamber" and
1732+
"combustion_chamber_to_nozzle". Default is
1733+
"nozzle_to_combustion_chamber".
1734+
1735+
Returns
1736+
-------
1737+
Generic Motor object
1738+
"""
1739+
if isinstance(file_name, str):
1740+
if path.splitext(path.basename(file_name))[1] == ".rse":
1741+
description, thrust_source = Motor.import_rse(file_name)
1742+
else:
1743+
raise ValueError("File must be a .rse file.")
1744+
else:
1745+
raise ValueError("File name must be a string.")
1746+
1747+
thrust = Function(thrust_source, "Time (s)", "Thrust (N)", "linear", "zero")
1748+
1749+
# handle eng parameters
1750+
if not chamber_radius:
1751+
chamber_radius = description["description"][
1752+
"diameter"
1753+
] # get motor diameter in meters
1754+
1755+
if not chamber_height:
1756+
chamber_height = description["description"][
1757+
"length"
1758+
] # get motor length in meters
1759+
1760+
if not propellant_initial_mass:
1761+
propellant_initial_mass = description["description"]["propellant_mass"]
1762+
1763+
if not dry_mass:
1764+
total_mass = description["description"]["total_mass"]
1765+
dry_mass = total_mass - propellant_initial_mass
1766+
1767+
if not nozzle_radius:
1768+
nozzle_radius = description["description"]["exit_diameter"]
1769+
1770+
return GenericMotor(
1771+
thrust_source=thrust,
1772+
burn_time=burn_time,
1773+
chamber_radius=chamber_radius,
1774+
chamber_height=chamber_height,
1775+
chamber_position=chamber_position,
1776+
propellant_initial_mass=propellant_initial_mass,
1777+
nozzle_radius=nozzle_radius,
1778+
dry_mass=dry_mass,
1779+
center_of_dry_mass_position=center_of_dry_mass_position,
1780+
dry_inertia=dry_inertia,
1781+
nozzle_position=nozzle_position,
1782+
reshape_thrust_curve=reshape_thrust_curve,
1783+
interpolation_method=interpolation_method,
1784+
coordinate_system_orientation=coordinate_system_orientation,
1785+
)
1786+
15691787
def all_info(self):
15701788
"""Prints out all data and graphs available about the Motor."""
15711789
# Print motor details

rocketpy/stochastic/stochastic_aero_surfaces.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
"""
55

66
from rocketpy.rocket.aero_surface import (
7+
AirBrakes,
78
EllipticalFins,
89
NoseCone,
910
RailButtons,
1011
Tail,
1112
TrapezoidalFins,
12-
AirBrakes,
1313
)
1414

1515
from .stochastic_model import StochasticModel

tests/unit/test_genericmotor.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,34 @@ def test_load_from_eng_file(generic_motor):
180180
assert generic_motor.thrust.y_array == pytest.approx(
181181
Function(points, "Time (s)", "Thrust (N)", "linear", "zero").y_array
182182
)
183+
184+
185+
def test_load_from_rse_file(generic_motor):
186+
"""Tests the GenericMotor.load_from_rse_file method.
187+
188+
Parameters
189+
----------
190+
generic_motor : rocketpy.GenericMotor
191+
The GenericMotor object to be used in the tests.
192+
"""
193+
194+
# Test the load_from_rse_file method
195+
generic_motor = generic_motor.load_from_rse_file(
196+
"data/motors/rse_example/rse_motor_example_file.rse"
197+
)
198+
199+
# Check if the engine has been loaded correctly
200+
assert generic_motor.thrust is not None
201+
assert generic_motor.dry_mass == 0.0363 # Total mass - propellant mass
202+
assert generic_motor.propellant_initial_mass == 0.0207
203+
assert generic_motor.burn_time == (0.0, 2.2)
204+
assert generic_motor.nozzle_radius == 0.00
205+
assert generic_motor.chamber_radius == 0.024
206+
assert generic_motor.chamber_height == 0.07
207+
208+
# Check the thrust curve values
209+
thrust_curve = generic_motor.thrust.source
210+
assert thrust_curve[0][0] == 0.0 # First time point
211+
assert thrust_curve[0][1] == 0.0 # First thrust point
212+
assert thrust_curve[-1][0] == 2.2 # Last point of time
213+
assert thrust_curve[-1][1] == 0.0 # Last thrust point

0 commit comments

Comments
 (0)