|
1 | 1 | import re
|
2 | 2 | import warnings
|
| 3 | +import xml.etree.ElementTree as ET |
3 | 4 | from abc import ABC, abstractmethod
|
4 | 5 | from functools import cached_property
|
5 | 6 | from os import path
|
@@ -268,16 +269,22 @@ class Function. Thrust units are Newtons.
|
268 | 269 | self.dry_I_13 = inertia[4]
|
269 | 270 | self.dry_I_23 = inertia[5]
|
270 | 271 |
|
271 |
| - # Handle .eng file inputs |
| 272 | + # Handle .eng or .rse file inputs |
272 | 273 | self.description_eng_file = None
|
| 274 | + self.rse_motor_data = None |
273 | 275 | if isinstance(thrust_source, str):
|
274 | 276 | if (
|
275 | 277 | path.exists(thrust_source)
|
276 | 278 | and path.splitext(path.basename(thrust_source))[1] == ".eng"
|
277 | 279 | ):
|
278 | 280 | _, self.description_eng_file, points = Motor.import_eng(thrust_source)
|
279 | 281 | 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 |
281 | 288 | # Evaluate raw thrust source
|
282 | 289 | self.thrust_source = thrust_source
|
283 | 290 | self.thrust = Function(
|
@@ -381,6 +388,10 @@ def dry_mass(self, dry_mass):
|
381 | 388 | self._dry_mass = float(self.description_eng_file[-2]) - float(
|
382 | 389 | self.description_eng_file[-3]
|
383 | 390 | )
|
| 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"]) |
384 | 395 | else:
|
385 | 396 | raise ValueError("Dry mass must be specified.")
|
386 | 397 |
|
@@ -990,6 +1001,83 @@ def clip_thrust(thrust, new_burn_time):
|
990 | 1001 | "zero",
|
991 | 1002 | )
|
992 | 1003 |
|
| 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 | + |
993 | 1081 | @staticmethod
|
994 | 1082 | def import_eng(file_name):
|
995 | 1083 | """Read content from .eng file and process it, in order to return the
|
@@ -1566,6 +1654,136 @@ def load_from_eng_file(
|
1566 | 1654 | coordinate_system_orientation=coordinate_system_orientation,
|
1567 | 1655 | )
|
1568 | 1656 |
|
| 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 | + |
1569 | 1787 | def all_info(self):
|
1570 | 1788 | """Prints out all data and graphs available about the Motor."""
|
1571 | 1789 | # Print motor details
|
|
0 commit comments