Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f0a647b
ENH: Improve error message for non-English .ork files
Gui-FernandesBR Mar 29, 2026
eb0f173
FIX: Include transition radii in rocket radius calculation
Gui-FernandesBR Mar 29, 2026
0322dfd
FIX: Use recursive search for Transition components in Java model
Gui-FernandesBR Mar 29, 2026
637cb8b
FIX: Use only first databranch for datapoint extraction
Gui-FernandesBR Mar 29, 2026
a3bfc5e
FIX: Handle integer position types and out-of-bounds in stored_results
Gui-FernandesBR Mar 29, 2026
fd9c152
ENH: Add 4 anonymous example rockets from databank
Gui-FernandesBR Mar 29, 2026
2bc07f8
ENH: Expand test suite to cover all 9 examples
Gui-FernandesBR Mar 29, 2026
df23983
ENH: Enable pytest in CI with Java 17 setup
Gui-FernandesBR Mar 29, 2026
de28bba
make lint
Gui-FernandesBR Mar 29, 2026
d1c8a7d
Add more examples to the database
Gui-FernandesBR Mar 29, 2026
923730e
delete wrong file
Gui-FernandesBR Mar 29, 2026
601c055
Fix: UnicodeDecodeError incorrect re-raise and raise e pattern
Gui-FernandesBR Jul 4, 2026
1f9cfff
Fix: Off-by-one slice error and loop break for ignition time
Gui-FernandesBR Jul 4, 2026
3e88fe6
Fix: Shell injection via os.system in nb_builder.py
Gui-FernandesBR Jul 4, 2026
edacb21
Fix: --verbose flag incorrect usage and propagation
Gui-FernandesBR Jul 4, 2026
0165038
Fix: Handle missing motor mass data and use calculated motor_dry_mass
Gui-FernandesBR Jul 4, 2026
bf08c1d
Fix: Add exception isolation in component extraction
Gui-FernandesBR Jul 4, 2026
d3bc8e7
Fix: Preserve noseShapeParameter in nosecone settings
Gui-FernandesBR Jul 4, 2026
eb02faf
Fix: Ensure bottom_radius is float in transition component
Gui-FernandesBR Jul 4, 2026
0c4045c
Fix: Add missing null checks for .find() using getattr
Gui-FernandesBR Jul 4, 2026
fcc1946
fix(parachute): set auto parachute CD to 0.75 fallback
Gui-FernandesBR Jul 4, 2026
1643ae3
fix(stored_results): do not clip negative values
Gui-FernandesBR Jul 4, 2026
75ca93f
Merge master into pr-41 and resolve conflicts
Gui-FernandesBR Jul 4, 2026
9bc336c
Fix pylint and getattr TypeErrors
Gui-FernandesBR Jul 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions rocketserializer/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ def parse_ork_file(ork_path: Path):
return bs, datapoints
except UnicodeDecodeError as exc:
error_msg = (
"The .ork file is not in UTF-8."
"The .ork file is not in UTF-8. "
+ "Please open the .ork file in a text editor and save it as UTF-8."
)
logger.error(error_msg)
raise UnicodeDecodeError(error_msg) from exc
raise ValueError(error_msg) from exc
except Exception as e:
logger.error("Error while parsing the file '%s': %s", ork_path.as_posix(), e)
raise e
raise


def _dict_to_string(dictionary, indent=0):
Expand Down
11 changes: 5 additions & 6 deletions rocketserializer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def cli():
help="The path to the OpenRocket .jar file.",
)
@click.option("--encoding", type=str, default="utf-8", required=False)
@click.option("--verbose", type=bool, default=False, required=False)
@click.option("--verbose", is_flag=True, default=False, help="Enable verbose logging")
def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=False):
"""Generates a .json file from the .ork file.
The .json file will be generated in the output folder using the information
Expand Down Expand Up @@ -216,8 +216,8 @@ def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=Fals
@click.option("--output", type=str, required=False)
@click.option("--ork_jar", type=str, default=None, required=False)
@click.option("--encoding", type=str, default="utf-8", required=False)
@click.option("--verbose", type=bool, default=False, required=False)
def ork2notebook(filepath, output, ork_jar=None, encoding="utf-8", verbose=False):
@click.option("--verbose", is_flag=True, default=False, help="Enable verbose logging")
def ork2notebook(filepath, output, ork_jar=None, encoding="utf-8", verbose=False): # pylint: disable=unused-argument
"""Generates a .ipynb file from the .ork file.

Notes
Expand All @@ -239,10 +239,9 @@ def ork2notebook(filepath, output, ork_jar=None, encoding="utf-8", verbose=False
"--output",
str(output),
"--encoding",
str(encoding),
"--verbose",
str(verbose),
]
if verbose:
args.append("--verbose")
if ork_jar:
args.extend(["--ork_jar", str(ork_jar)])

Expand Down
16 changes: 8 additions & 8 deletions rocketserializer/components/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ def search_environment(bs):
"""
settings = {}

latitude = float(bs.find("launchlatitude").text)
longitude = float(bs.find("launchlongitude").text)
elevation = float(bs.find("launchaltitude").text)
wind_average = float(bs.find("windaverage").text)
wind_turbulence = float(bs.find("windturbulence").text)
geodetic_method = bs.find("geodeticmethod").text
latitude = float(getattr(bs.find("launchlatitude"), "text", "0"))
longitude = float(getattr(bs.find("launchlongitude"), "text", "0"))
elevation = float(getattr(bs.find("launchaltitude"), "text", "0"))
wind_average = float(getattr(bs.find("windaverage"), "text", "0"))
wind_turbulence = float(getattr(bs.find("windturbulence"), "text", "0"))
geodetic_method = getattr(bs.find("geodeticmethod"), "text", "")
logger.info(
"Collected first environment settings: latitude, "
+ "longitude, elevation, wind_average, wind_turbulence, geodetic_method"
)
try:
base_temperature = float(bs.find("basetemperature").text)
base_temperature = float(getattr(bs.find("basetemperature"), "text", "0"))
logger.info(
"The base temperature was found in the .ork file. It is %f °C.",
base_temperature,
Expand All @@ -46,7 +46,7 @@ def search_environment(bs):
)
base_temperature = None
try:
base_pressure = float(bs.find("basepressure").text)
base_pressure = float(getattr(bs.find("basepressure"), "text", "0"))
logger.info(
"The base pressure was found in the .ork file. It is %f Pa.",
base_pressure,
Expand Down
34 changes: 19 additions & 15 deletions rocketserializer/components/fins.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def search_trapezoidal_fins(bs, elements):
"Starting collecting the settings for the trapezoidal fin set number '%d'",
idx,
)
label = fin.find("name").text
label = getattr(fin.find("name"), "text", "")
try:

def get_element_by_name(name):
Expand All @@ -57,32 +57,36 @@ def get_element_by_name(name):
logger.error(message)
raise KeyError(message)

n_fin = int(fin.find("fincount").text)
n_fin = int(getattr(fin.find("fincount"), "text", "0"))
logger.info("Number of fins retrieved: %d", n_fin)

root_chord = float(fin.find("rootchord").text)
root_chord = float(getattr(fin.find("rootchord"), "text", "0"))
logger.info("Root chord retrieved: %f", root_chord)

tip_chord = float(fin.find("tipchord").text)
tip_chord = float(getattr(fin.find("tipchord"), "text", "0"))
logger.info("Tip chord retrieved: %f", tip_chord)

span = float(fin.find("height").text)
span = float(getattr(fin.find("height"), "text", "0"))
logger.info("Span retrieved: %f", span)

sweep_length = (
float(fin.find("sweeplength").text) if fin.find("sweeplength") else None
float(getattr(fin.find("sweeplength"), "text", "0"))
if fin.find("sweeplength")
else None
)
sweep_angle = (
float(fin.find("sweepangle").text) if fin.find("sweepangle") else None
float(getattr(fin.find("sweepangle"), "text", "0"))
if fin.find("sweepangle")
else None
)
logger.info(
"Sweep length and angle retrieved: %s, %s", sweep_length, sweep_angle
)

cant_angle = float(fin.find("cant").text)
cant_angle = float(getattr(fin.find("cant"), "text", "0"))
logger.info("Cant angle retrieved: %f", cant_angle)

section = fin.find("crosssection").text
section = getattr(fin.find("crosssection"), "text", "")
logger.info("Crosssection format retrieved")

fin_settings = {
Expand Down Expand Up @@ -145,7 +149,7 @@ def search_elliptical_fins(bs, elements):
"Starting collecting the settings for the elliptical fin set number '%d'",
idx,
)
label = fin.find("name").text
label = getattr(fin.find("name"), "text", "")
try:

def get_element_by_name(name):
Expand All @@ -164,19 +168,19 @@ def get_element_by_name(name):
logger.error(message)
raise KeyError(message)

n_fin = int(fin.find("fincount").text)
n_fin = int(getattr(fin.find("fincount"), "text", "0"))
logger.info("Number of fins retrieved: %d", n_fin)

root_chord = float(fin.find("rootchord").text)
root_chord = float(getattr(fin.find("rootchord"), "text", "0"))
logger.info("Root chord retrieved: %f", root_chord)

span = float(fin.find("height").text)
span = float(getattr(fin.find("height"), "text", "0"))
logger.info("Span retrieved: %f", span)

cant_angle = float(fin.find("cant").text)
cant_angle = float(getattr(fin.find("cant"), "text", "0"))
logger.info("Cant angle retrieved: %f", cant_angle)

section = fin.find("crosssection").text
section = getattr(fin.find("crosssection"), "text", "")
logger.info("Crosssection format retrieved")

fin_settings = {
Expand Down
6 changes: 3 additions & 3 deletions rocketserializer/components/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ def search_launch_conditions(bs):
"""
settings = {}

launch_rod_length = float(bs.find("launchrodlength").text)
launch_rod_angle = float(bs.find("launchrodangle").text)
launch_rod_direction = float(bs.find("launchroddirection").text)
launch_rod_length = float(getattr(bs.find("launchrodlength"), "text", "0"))
launch_rod_angle = float(getattr(bs.find("launchrodangle"), "text", "0"))
launch_rod_direction = float(getattr(bs.find("launchroddirection"), "text", "0"))
logger.info(
"Collected launch conditions: launch rod length, launch rod angle, "
"launch rod direction."
Expand Down
37 changes: 24 additions & 13 deletions rocketserializer/components/id.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,31 @@ def search_id_info(bs, filepath):
"filepath".
"""
settings = {}
settings["rocket_name"] = bs.find("rocket").find("name").text
logger.info("Collected the rocket name: '%s'", settings["rocket_name"])

try:
settings["comment"] = bs.find("rocket").find("comment").text.replace("\n", "")
logger.info("Collected the comment saved in the file: %s", settings["comment"])
except AttributeError:
logger.warning("No auxiliary comment was found in the file.")
rocket_tag = bs.find("rocket")
if rocket_tag:
settings["rocket_name"] = getattr(rocket_tag.find("name"), "text", "")
logger.info("Collected the rocket name: '%s'", settings["rocket_name"])

comment_tag = rocket_tag.find("comment")
if comment_tag and comment_tag.text:
settings["comment"] = comment_tag.text.replace("\n", "")
logger.info(
"Collected the comment saved in the file: %s", settings["comment"]
)
else:
logger.warning("No auxiliary comment was found in the file.")
settings["comment"] = None

designer_tag = rocket_tag.find("designer")
if designer_tag and designer_tag.text:
settings["designer"] = designer_tag.text
logger.info("Collected the designer name: %s", settings["designer"])
else:
logger.warning("No designer name was found in the file.")
settings["designer"] = None
else:
settings["rocket_name"] = ""
settings["comment"] = None
try:
settings["designer"] = bs.find("rocket").find("designer").text
logger.info("Collected the designer name: %s", settings["designer"])
except AttributeError:
logger.warning("No designer name was found in the file.")
settings["designer"] = None
# settings["ork_version"] = bs.attrs["creator"]
settings["filepath"] = Path(filepath).as_posix()
Expand Down
15 changes: 12 additions & 3 deletions rocketserializer/components/motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,18 @@ def search_motor(bs, datapoints, data_labels):
settings = {}

# retrieve motor geometry
motor_length = float(bs.find("motormount").find("length").text)
motor_radius = float(bs.find("motormount").find("diameter").text) / 2
motormount = bs.find("motormount")
if motormount is not None:
motor_length = float(getattr(motormount.find("length"), "text", "") or "0")
diam = getattr(motormount.find("diameter"), "text", "") or "0"
motor_radius = float(diam) / 2
else:
motor_length = 0.0
motor_radius = 0.0
logger.info("Collected motor geometry: motor length and motor radius.")

# get motor mass properties
total_propellant_mass, motor_dry_mass, _ = __get_motor_mass(datapoints, data_labels)
motor_dry_mass = 0 # If NOTE: dry inertia is 0, this should ALWAYS be 0 too.
center_of_dry_mass = 0
dry_inertia = (0, 0, 0) # impossible to retrieve from .ork file

Expand Down Expand Up @@ -183,6 +188,10 @@ def __get_motor_mass(datapoints, data_labels):
prop_mass_vector = motor_mass - motor_dry_mass
prop_mass_vector = list(prop_mass_vector)
logger.info("The motor dry mass is %.3f kg.", motor_dry_mass)
else:
raise ValueError(
"Neither 'Propellant mass' nor 'Motor mass' found in data_labels."
)

normalize = np.array(prop_mass_vector)
normalize = normalize - normalize[np.argmin(normalize)]
Expand Down
19 changes: 12 additions & 7 deletions rocketserializer/components/nose_cone.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ def search_nosecone(bs, elements=None, rocket_radius=None, just_radius=False):
"""
settings = {}
nosecone = bs.find("nosecone") # TODO: allow for multiple nosecones
name = nosecone.find("name").text if nosecone else "nosecone"
name = getattr(nosecone.find("name"), "text", "") if nosecone else "nosecone"

if not nosecone:
nosecones = list(
filter(
lambda x: x.find("name").text == "Nosecone", bs.find_all("transition")
lambda x: getattr(x.find("name"), "text", "") == "Nosecone",
bs.find_all("transition"),
)
)
if len(nosecones) == 0:
Expand All @@ -41,9 +42,9 @@ def search_nosecone(bs, elements=None, rocket_radius=None, just_radius=False):
logger.info("Multiple nosecones found, using only the first one")
nosecone = nosecones[0] # only the first nosecone is considered

length = float(nosecone.find("length").text)
kind = nosecone.find("shape").text
base_radius = nosecone.find("aftradius").text
length = float(getattr(nosecone.find("length"), "text", "0"))
kind = getattr(nosecone.find("shape"), "text", "")
base_radius = getattr(nosecone.find("aftradius"), "text", "")
try:
base_radius = float(base_radius)
except ValueError:
Expand Down Expand Up @@ -83,10 +84,11 @@ def get_position(name, length):
if kind == "haack":
logger.info("Nosecone is a haack nosecone, searching for the shape parameter")

shape_parameter = float(nosecone.find("shapeparameter").text)
shape_parameter = float(getattr(nosecone.find("shapeparameter"), "text", "0"))
kind = "Von Karman" if shape_parameter == 0.0 else "lvhaack"
settings.update({"noseShapeParameter": shape_parameter})
logger.info("Shape parameter of the nosecone: %s", shape_parameter)
else:
shape_parameter = None

settings = {
"name": name,
Expand All @@ -95,5 +97,8 @@ def get_position(name, length):
"base_radius": base_radius,
"position": get_position(name, length),
}
if shape_parameter is not None:
settings["noseShapeParameter"] = shape_parameter

logger.info("Nosecone setting defined:\n %s", _dict_to_string(settings, indent=23))
return settings
34 changes: 16 additions & 18 deletions rocketserializer/components/parachute.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,24 @@ def search_parachutes(bs):

for idx, chute in enumerate(chutes):
logger.info("Starting to collect the settings of the parachute number %d", idx)
name = chute.find("name").text
name = getattr(chute.find("name"), "text", "")

# parachute settings
cd = "auto" if "auto" in chute.find("cd").text else float(chute.find("cd").text)
cd = (
"auto"
if "auto" in getattr(chute.find("cd"), "text", "")
else float(getattr(chute.find("cd"), "text", "0"))
)
cd = search_cd_chute_if_auto(chute) if cd == "auto" else cd
area = np.pi * float(chute.find("diameter").text) ** 2 / 4
area = np.pi * float(getattr(chute.find("diameter"), "text", "0")) ** 2 / 4
cds = cd * area
logger.info("Parachute '%s' has a drag coefficient of %f", name, cd)

# deployment settings
deploy_event = chute.find("deployevent").text
deploy_delay = float(chute.find("deploydelay").text)
deploy_event = getattr(chute.find("deployevent"), "text", "")
deploy_delay = float(getattr(chute.find("deploydelay"), "text", "0"))
deploy_altitude = (
float(chute.find("deployaltitude").text)
float(getattr(chute.find("deployaltitude"), "text", "0"))
if deploy_event == "altitude"
else None
)
Expand All @@ -70,17 +74,11 @@ def search_parachutes(bs):


def search_cd_chute_if_auto(bs):
# if the parachute cd is st to "auto", then look for the cd in the next tag
# return float(
# next(
# filter(lambda x: x.text.replace(".", "").isnumeric(), bs.findAll("cd"))
# ).text
# )

# TODO: for the future, we need to check if the ork object has a drag coefficient

# simply return 1.0
# if the parachute cd is set to "auto", OpenRocket defaults to 0.75 (flat)
# or 1.5 (dome). Since we cannot easily deduce the type, 0.75 is the
# most common standard parachute CD in OR.
logger.warning(
"cd auto: the cd is set to 1.0 for parachute %s", bs.find("name").text
"cd auto: the cd is set to 0.75 for parachute %s",
getattr(bs.find("name"), "text", "Unknown"),
)
return 1.0
return 0.75
4 changes: 2 additions & 2 deletions rocketserializer/components/rail_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def search_rail_buttons(bs, elements: dict) -> dict:
angular_position = 0.0
lugs = bs.find_all("launchlug")
for lug in lugs:
if lug.find("name").text == name:
angular_position = float(lug.find("radialdirection").text)
if getattr(lug.find("name"), "text", "") == name:
angular_position = float(getattr(lug.find("radialdirection"), "text", "0"))
break

return {
Expand Down
8 changes: 6 additions & 2 deletions rocketserializer/components/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ def get_rocket_radius(bs):
noses = bs.find_all("nosecone")
transitions = bs.find_all("transition")

tubes_radius = [i.find("radius").text for i in tubes if i.find("radius")]
noses_radius = [i.find("aftradius").text for i in noses if i.find("aftradius")]
tubes_radius = [
getattr(i.find("radius"), "text", "") for i in tubes if i.find("radius")
]
noses_radius = [
getattr(i.find("aftradius"), "text", "") for i in noses if i.find("aftradius")
]

# Also collect radii from transitions (foreradius and aftradius)
transition_radius = []
Expand Down
Loading
Loading