diff --git a/data/lazarus/lazarus-uncompressed/rocket.ork b/data/lazarus/lazarus-uncompressed/rocket.ork new file mode 100644 index 000000000..c840157b1 --- /dev/null +++ b/data/lazarus/lazarus-uncompressed/rocket.ork @@ -0,0 +1,283 @@ + + + + Rocket + + + + maximum + + + + Sustainer + 24.757071532768 + 2.3114 + true + + + + Nose cone + 1.612 + false + smooth + Cardboard + 0.6687312 + 0.002 + haack + false + 0.0 + 0.11137899999999999 + 0.0 + 0.0 + 0.0 + false + + + + Fin Can + 3.056 + false + smooth + Cardboard + 3.2258 + 0.002 + auto + + automatic + 0.0 + 0.0 + + TAMUSRT + 78c3a4a99e8a9f23471d80cde35aa4ac + N27 + 0.0577 + 2.2918000000000003 + none + + + TAMUSRT-30k + 61987aa8d66e09e7e9842c644c1ee006 + N27-30k + 0.0577 + 2.2918000000000003 + none + + + AeroTech + f097ce447c4666d6124670f2768d667a + N2000W + 0.098 + 1.046 + 0.0 + + + + + + Trapezoidal fin set + 3.4798 + smooth + Cardboard + 3 + 0.0 + 0.0254 + airfoil + 0.0 + 0.0 + Cardboard + 0.30479999999999996 + 0.07619999999999999 + 0.22352 + 0.2794 + + + + Drogue + 0.0 + 0.025 + 0.0125 + 0.0 + 0.0 + 2.2 + Ripstop nylon + apogee + 200.0 + 1.0 + 2.032 + 6 + 0.3 + Elastic cord (round 2 mm, 1/16 in) + + + + Main + 0.0 + 0.025 + 0.0125 + 0.0 + 0.0 + auto + Ripstop nylon + altitude + 304.8 + 0.0 + 0.3 + 6 + 0.3 + Elastic cord (round 2 mm, 1/16 in) + + + + + + Transition + smooth + Cardboard + 0.23622 + 0.002 + ellipsoid + true + auto + 0.073152 + 0.0 + 0.0 + 0.0 + false + 0.0 + 0.0 + 0.0 + false + + + + + + + + + Simulation 1 + RK4Simulator + BarrowmanCalculator + + bcb82022-f301-40de-8cae-e068b6c72e12 + 1.0 + 0.0 + 90.0 + 2.0 + 0.1 + 0.0 + 28.61 + -80.6 + spherical + + 0.05 + + + + + Simulation 2 + RK4Simulator + BarrowmanCalculator + + bcb82022-f301-40de-8cae-e068b6c72e12 + 1.0 + 0.0 + 90.0 + 2.0 + 0.1 + 0.0 + 28.61 + -80.6 + spherical + + 0.05 + + + + + + + Simulation 3 + RK4Simulator + BarrowmanCalculator + + c872ec37-7f90-437c-9a46-d85ae9c25680 + 1.0 + 0.0 + 90.0 + 2.0 + 0.1 + 0.0 + 28.61 + -80.6 + spherical + + 0.05 + + + + + Simulation 4 + RK4Simulator + BarrowmanCalculator + + c872ec37-7f90-437c-9a46-d85ae9c25680 + 1.0 + 0.0 + 90.0 + 2.0 + 0.1 + 0.0 + 28.61 + -80.6 + spherical + + 0.05 + + + + + + + Simulation 5 + RK4Simulator + BarrowmanCalculator + + c872ec37-7f90-437c-9a46-d85ae9c25680 + 9.144 + 0.0 + 90.0 + 2.0 + 0.1 + 0.0 + 28.61 + -80.6 + spherical + + 0.05 + + + + + + + Simulation 6 + RK4Simulator + BarrowmanCalculator + + 45097303-41ec-4952-8480-1aec9cbb95cd + 1.0 + 0.0 + 90.0 + 2.0 + 0.1 + 0.0 + 28.61 + -80.6 + spherical + + 0.05 + + + + diff --git a/data/lazarus/lazarus.ork b/data/lazarus/lazarus.ork new file mode 100644 index 000000000..cf1108ad2 Binary files /dev/null and b/data/lazarus/lazarus.ork differ diff --git a/deserializer.py b/deserializer.py new file mode 100644 index 000000000..27c75e7f8 --- /dev/null +++ b/deserializer.py @@ -0,0 +1,138 @@ +""" +Java deserializer +adapted from StackOverflow response: http://stackoverflow.com/a/16470856/3324704 + +This in theory should deserialize the OpenRocket thrust curve file, but I couldn't get it to work. +""" + +def parse(f): + h = lambda s: ' '.join('%.2X' % ord(x) for x in s) # format as hex + p = lambda s: sum(ord(x)*256**i for i, x in enumerate(reversed(s))) # parse integer + magic = f.read(2) + assert magic == '\xAC\xED', h(magic) # STREAM_MAGIC + assert p(f.read(2)) == 5 # STREAM_VERSION + handles = [] + def parse_obj(): + b = f.read(1) + if not b: + raise StopIteration # not necessarily the best thing to throw here. + if b == '\x70': # p TC_NULL + return None + elif b == '\x71': # q TC_REFERENCE + handle = p(f.read(4)) - 0x7E0000 # baseWireHandle + o = handles[handle] + return o[1] + elif b == '\x74': # t TC_STRING + string = f.read(p(f.read(2))).decode('utf-8') + handles.append(('TC_STRING', string)) + return string + elif b == '\x75': # u TC_ARRAY + data = [] + cls = parse_obj() + size = p(f.read(4)) + handles.append(('TC_ARRAY', data)) + assert cls['_name'] in ('[B', '[I'), (cls['_name'], size, f.read(50)) + for x in range(size): + data.append(f.read({'[B': 1, '[I': 4}[cls['_name']])) + return data + elif b == '\x7E': # ~ TC_ENUM + enum = {} + enum['_cls'] = parse_obj() + handles.append(('TC_ENUM', enum)) + enum['_name'] = parse_obj() + return enum + elif b == '\x72': # r TC_CLASSDESC + cls = {'fields': []} + full_name = f.read(p(f.read(2))) + cls['_name'] = full_name.split('.')[-1] # i don't care about full path + f.read(8) # uid + cls['flags'] = f.read(1) + handles.append(('TC_CLASSDESC', cls)) + assert cls['flags'] in ('\2', '\3', '\x0C', '\x12'), h(cls['flags']) + b = f.read(2) + for i in range(p(b)): + typ = f.read(1) + name = f.read(p(f.read(2))) + fcls = parse_obj() if typ in 'L[' else '' + cls['fields'].append((name, typ, fcls.split('/')[-1])) # don't care about full path + b = f.read(1) + assert b == '\x78', h(b) + cls['parent'] = parse_obj() + return cls + # TC_OBJECT + assert b == '\x73', (h(b), h(f.read(4)), f.read(50)) + obj = {} + obj['_cls'] = parse_obj() + obj['_name'] = obj['_cls']['_name'] + handle = len(handles) + parents = [obj['_cls']] + while parents[0]['parent']: + parents.insert(0, parents[0]['parent']) + handles.append(('TC_OBJECT', obj)) + for cls in parents: + for name, typ, fcls in cls['fields'] if cls['flags'] in ('\2', '\3') else []: + if typ == 'I': # Integer + obj[name] = p(f.read(4)) + elif typ == 'S': # Short + obj[name] = p(f.read(2)) + elif typ == 'J': # Long + obj[name] = p(f.read(8)) + elif typ == 'Z': # Bool + b = f.read(1) + assert p(b) in (0, 1) + obj[name] = bool(p(b)) + elif typ == 'F': # Float + obj[name] = h(f.read(4)) + elif typ in 'BC': # Byte, Char + obj[name] = f.read(1) + elif typ in 'L[': # Object, Array + obj[name] = parse_obj() + else: # Unknown + assert False, (name, typ, fcls) + if cls['flags'] in ('\3', '\x0C'): # SC_WRITE_METHOD, SC_BLOCKDATA + b = f.read(1) + if b == '\x77': # see the readObject / writeObject methods + block = f.read(p(f.read(1))) + if cls['_name'].endswith('HashMap') or cls['_name'].endswith('Hashtable'): + # http://javasourcecode.org/html/open-source/jdk/jdk-6u23/java/util/HashMap.java.html + # http://javasourcecode.org/html/open-source/jdk/jdk-6u23/java/util/Hashtable.java.html + assert len(block) == 8, h(block) + size = p(block[4:]) + obj['data'] = [] # python doesn't allow dicts as keys + for i in range(size): + k = parse_obj() + v = parse_obj() + obj['data'].append((k, v)) + try: + obj['data'] = dict(obj['data']) + except TypeError: + pass # non hashable keys + elif cls['_name'].endswith('HashSet'): + # http://javasourcecode.org/html/open-source/jdk/jdk-6u23/java/util/HashSet.java.html + assert len(block) == 12, h(block) + size = p(block[-4:]) + obj['data'] = [] + for i in range(size): + obj['data'].append(parse_obj()) + elif cls['_name'].endswith('ArrayList'): + # http://javasourcecode.org/html/open-source/jdk/jdk-6u23/java/util/ArrayList.java.html + assert len(block) == 4, h(block) + obj['data'] = [] + for i in range(obj['size']): + obj['data'].append(parse_obj()) + else: + assert False, cls['_name'] + b = f.read(1) + assert b == '\x78', h(b) + ' ' + repr(f.read(50)) # TC_ENDBLOCKDATA + handles[handle] = ('py', obj) + return obj + objs = [] + while 1: + try: + objs.append(parse_obj()) + except StopIteration: + return objs + +with open("data/lazarus/thrustcurves.ser", "rb") as f: + print(f.read(2)) + parse(f) \ No newline at end of file diff --git a/docs/notebooks/getting_started.ipynb b/docs/notebooks/getting_started.ipynb index e1e9d45ea..4b3bb8103 100644 --- a/docs/notebooks/getting_started.ipynb +++ b/docs/notebooks/getting_started.ipynb @@ -1412,7 +1412,7 @@ "metadata": { "hide_input": false, "kernelspec": { - "display_name": "Python 3.10.0 ('rocketpy_dev')", + "display_name": "Python 3.8.13 ('rocketPyHackathon')", "language": "python", "name": "python3" }, @@ -1426,11 +1426,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.0" + "version": "3.8.13" }, "vscode": { "interpreter": { - "hash": "18e93d5347af13ace37d47ea4e2a2ad720f0331bd9cb28f9983f5585f4dfaa5c" + "hash": "79c2bdae762d28375c24b6ed4e1bb7c973109646b4476cc96b1fe5987377f03d" } } }, diff --git a/ork_testing.py b/ork_testing.py new file mode 100644 index 000000000..f26a9e50a --- /dev/null +++ b/ork_testing.py @@ -0,0 +1,70 @@ +from rocketpy import Environment, SolidMotor, Rocket, utilities, Flight +import datetime + +# Import ork +ork_file_path = 'data/lazarus/lazarus.ork' +ork_xml_obj = utilities.import_openrocket(ork_file_path) + +# Create environment +simulation_number = 5 +Env = utilities.ork_create_environment(ork_xml_obj, simulation_number) + +tomorrow = datetime.date.today() + datetime.timedelta(days=1) +Env.setDate((tomorrow.year, tomorrow.month, tomorrow.day, 12)) # Hour given in UTC time + +# Create Motor +# I didn't have time to add motor functionality since the OpenRocket +# motor files are in a serialized Java ArrayList. I attempted to deserialize +# the motor data and failed by the time PRs were due. I have included a deserializer.py +# file but it does not work. +Pro75M1670 = SolidMotor( + thrustSource="data/motors/Cesaroni_M1670.eng", + burnOut=3.9, + grainNumber=5, + grainSeparation=5 / 1000, + grainDensity=1815, + grainOuterRadius=33 / 1000, + grainInitialInnerRadius=15 / 1000, + grainInitialHeight=120 / 1000, + nozzleRadius=33 / 1000, + throatRadius=11 / 1000, + interpolationMethod="linear", +) + +# Create rocket +# since intertia and drag curves cannot be extracted from OpenRocket, +# it is best for users to manually define the Rocket Obj. +# Drag curves can be made from OpenRocket but not extracted from +# an .ork file +ork_rocket = Rocket( + motor=Pro75M1670, + radius=127 / 2000, + mass=19.197 - 2.956, + inertiaI=6.60, + inertiaZ=0.0351, + distanceRocketNozzle=-1.255, + distanceRocketPropellant=-0.85704, + powerOffDrag="data/calisto/powerOffDragCurve.csv", + powerOnDrag="data/calisto/powerOnDragCurve.csv", +) +ork_rocket.setRailButtons([0.2, -0.5]) + +# Add Aero Surfaces +# The aero surfaces' distance to CM cannot be extracted from .ork files +# and needs to be manually set by the user +nose_distanceToCM = 0.71971 +fin_distanceToCM = -1.04956 +ork_rocket = utilities.ork_add_aero_surfaces(ork_rocket, ork_xml_obj, nose_distanceToCM, fin_distanceToCM) + +# openRocket does not have a standard method for defining tail cones +# so users should define their tailcone separately +Tail = ork_rocket.addTail( + topRadius=0.0635, bottomRadius=0.0435, length=0.060, distanceToCM=-1.194656 +) + +# Add parachutes +ork_rocket = utilities.ork_add_parachutes(ork_rocket, ork_xml_obj, Env) + +#Test Flight +TestFlight = Flight(rocket=ork_rocket, environment=Env, inclination=85, heading=0) +TestFlight.allInfo() diff --git a/rocketpy/utilities.py b/rocketpy/utilities.py index dc847ea3c..f54434c12 100644 --- a/rocketpy/utilities.py +++ b/rocketpy/utilities.py @@ -3,13 +3,234 @@ __copyright__ = "Copyright 20XX, RocketPy Team" __license__ = "MIT" +from cgi import test import numpy as np from scipy.integrate import solve_ivp +import zipfile +import xml.etree.ElementTree as ET +from math import pi from .Environment import Environment from .Function import Function +def import_openrocket( + ork_file_path +): + """Decompresses an OpenRocket .ork file and saves the uncompressed file for future use. + + Parameters + ---------- + ork_file_path : string + The directory location of the .ork file, including the file extension. + + Returns + ------- + ork_xml_obj : xml.etree.ElementTree.Element + An cml Element Tree object of the ork data. + + """ + + + # remove .ork extension and add -uncompressed to create new directory name + ork_root_path = ork_file_path[0:-4] + '-uncompressed' + + with zipfile.ZipFile(ork_file_path, "r") as compressed_ork_file: + compressed_ork_file.extractall(ork_root_path) + + with open(ork_root_path+"/rocket.ork", "r") as uncompressed_ork_file: + ork_xml_data = uncompressed_ork_file.read() + + ork_xml_obj = ET.fromstring(ork_xml_data) + + return ork_xml_obj + + +def ork_create_environment( + ork_xml_obj, simulation_number +): + """Creates a RocketPy Environment class object based on parameters from a given OpenRocket file. + + Parameters + ---------- + ork_xml_obj : xml.etree.ElementTree.Element + An cml Element Tree object of the ork data. + simulation_number : int + The number simulation in the OpenRocket file that the users wishes to extract environment conditions from. Can be 1-N. + + Returns + ------- + ork_environment : Environment + An Environment object containing all openRocket environment parameters + + """ + + + # parse environment parameters + simulations_list = ork_xml_obj[1].findall('simulation') + if simulation_number <= 0 or simulation_number > len(simulations_list): + er_msg = 'simulation_number of {} is not valid. Simulation #{} is not present in the OpenRocket file.'.format(simulation_number) + raise ValueError(er_msg) + + parameter_simulation = simulations_list[simulation_number] + ork_railLength = float(parameter_simulation.find("conditions").find("launchrodlength").text) + ork_latitude = float(parameter_simulation.find("conditions").find("launchlatitude").text) + ork_longitude = float(parameter_simulation.find("conditions").find("launchlongitude").text) + ork_elevation= float(parameter_simulation.find("conditions").find("launchaltitude").text) + + ork_environment = Environment( + railLength=ork_railLength, latitude=ork_latitude, longitude=ork_longitude, elevation=ork_elevation + ) + + return ork_environment + + +def ork_add_aero_surfaces( + rocket, ork_xml_obj, nose_distanceToCM, fin_distanceToCM +): + """Returns a Rocket object with the added aerodynamic surfaces nosecone and finset. Note: This does not + currently support adding tail aero surfaces because OpenRocket does not have a standard tailcone parameter. + Additionally, this function requires the distanceToCM values for the nosecone and fins because this is also + not provided by OpenRocket. + + Parameters + ---------- + rocket : Rocket + A RocketPy Rocket object. + ork_xml_obj : xml.etree.ElementTree.Element + An cml Element Tree object of the ork data. + nose_distanceToCM : int + Distance from nosecone base to center of mass. + fin_distanceToCM : int + Distance from fins to center of mass. + + Returns + ------- + rocket : Rocket + A RocketPy Rocket object with nosecone and finset surfaces added. + + """ + + ork_nose_length = 0 + ork_nose_kind = "" + ork_num_fins = 0 + ork_fin_span = 0 + ork_rootChord = 0 + ork_tipChord = 0 + + for subcomponent in ork_xml_obj[0].findall('subcomponents'): + for stage in subcomponent.findall('stage'): + for stage_subcomponent in stage.findall('subcomponents'): + for geometry in stage_subcomponent: + # Add nosecone parameters + if geometry.tag == "nosecone": + ork_nose_length = float(geometry.find("length").text) + ork_nose_kind = geometry.find("shape").text + if ork_nose_kind == "haack": + ork_nose_kind = "lvhaack" + # TODO, double check from OpenRocket that conical and ogive don't need to be renamed as well + else: + raise ValueError('Nosecone type of "{}" is not supported'.format(ork_nose_kind)) + + # Add fin geometries + # Note: does not support eliptical or freeform fins + elif geometry.tag == "bodytube": + for geom_subcomponent in geometry.findall('subcomponents'): + for finset in geom_subcomponent.findall('trapezoidfinset'): + ork_num_fins = float(finset.find("fincount").text) + ork_fin_span = float(finset.find("height").text) + ork_rootChord = float(finset.find("rootchord").text) + ork_tipChord = float(finset.find("tipchord").text) + + NoseCone = rocket.addNose(length=ork_nose_length, kind=ork_nose_kind, distanceToCM=nose_distanceToCM) + + FinSet = rocket.addFins( + ork_num_fins, span=ork_fin_span, rootChord=ork_rootChord, tipChord=ork_tipChord, distanceToCM=fin_distanceToCM + ) + + return rocket + + +def ork_add_parachutes( + rocket, ork_xml_obj, environment +): + """Returns a Rocket object with the added parachutes. + + Parameters + ---------- + rocket : Rocket + A RocketPy Rocket object. + ork_xml_obj : xml.etree.ElementTree.Element + An cml Element Tree object of the ork data. + environment : Environment + A RocketPy Environment object. + + Returns + ------- + rocket : Rocket + A RocketPy Rocket object with parachutes. + + """ + + # TODO find workaround for OpenRocket "auto" value for cd + for subcomponent in ork_xml_obj[0].findall('subcomponents'): + for stage in subcomponent.findall('stage'): + for stage_subcomponent in stage.findall('subcomponents'): + for bodytube in stage_subcomponent.findall('bodytube'): + for body_subcomponent in bodytube.findall('subcomponents'): + for parachute in body_subcomponent.findall('parachute'): + deployevent_type = parachute.find("deployevent").text + if deployevent_type == "apogee": + ork_drogue_cd = parachute.find("cd").text + if ork_drogue_cd == "auto": + ork_drogue_cd = 1.2 + else: + ork_drogue_cd = float(ork_drogue_cd) + ork_drogue_diameter = float(parachute.find("diameter").text) + ork_drogue_cds = ork_drogue_cd * pi * (ork_drogue_diameter/2)**2 + elif deployevent_type == "altitude": + ork_main_cd = parachute.find("cd").text + if ork_main_cd == "auto": + ork_main_cd = 1.2 + else: + ork_main_cd = float(ork_main_cd) + ork_main_diameter = float(parachute.find("diameter").text) + ork_main_cds = ork_main_cd * pi * (ork_main_diameter/2)**2 + ork_main_altitude = float(parachute.find("deployaltitude").text) + else: + raise ValueError('Unknown parachute deployment type of {}'.format(deployevent_type)) + + + def drogueTrigger(p, y): + # p = pressure + # y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3] + # activate drogue when vz < 0 m/s. + return True if y[5] < 0 else False + + # TODO find a way for the main trigger to be set off by ork_main_altitude + environment.elevation instead of constants + # this may require rewriting how the trigger is used during flight simulation + def mainTrigger(p, y): + # p = pressure + # y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3] + # activate main when vz < 0 m/s and z < 800 + 1400 m (+1400 due to surface elevation). + return True if y[5] < 0 and y[2] < 800 + 1400 else False + + + Main = rocket.addParachute( + "Main", + CdS=ork_main_cds, + trigger=mainTrigger, + ) + + Drogue = rocket.addParachute( + "Drogue", + CdS=ork_drogue_cds, + trigger=drogueTrigger, + ) + + return rocket + + # TODO: Needs tests def compute_CdS_from_drop_test( terminal_velocity, rocket_mass, air_density=1.225, g=9.80665