@@ -504,6 +504,7 @@ def __init__( # pylint: disable=too-many-arguments,too-many-statements
504504 verbose = False ,
505505 name = "Flight" ,
506506 equations_of_motion = "standard" ,
507+ use_udot_rail2 = True ,
507508 ode_solver = "LSODA" ,
508509 simulation_mode = "6 DOF" ,
509510 ):
@@ -581,6 +582,12 @@ def __init__( # pylint: disable=too-many-arguments,too-many-statements
581582 more restricted set of equations of motion that only works for
582583 solid propulsion rockets. Such equations were used in RocketPy v0
583584 and are kept here for backwards compatibility.
585+ use_udot_rail2 : bool, optional
586+ If True, enable the intermediate 3-DOF rail phase ``udot_rail2``
587+ (tip-off analysis). If False, the flight remains in the 1-DOF
588+ ``udot_rail1`` phase until the upper rail button exit and then
589+ transitions directly to the generalized 6-DOF dynamics, as in
590+ previous versions. Default is True.
584591 ode_solver : str, ``scipy.integrate.OdeSolver``, optional
585592 Integration method to use to solve the equations of motion ODE.
586593 Available options are: 'RK23', 'RK45', 'DOP853', 'Radau', 'BDF',
@@ -626,6 +633,8 @@ def __init__( # pylint: disable=too-many-arguments,too-many-statements
626633 self .equations_of_motion = equations_of_motion
627634 self .simulation_mode = simulation_mode
628635 self .ode_solver = ode_solver
636+ # Enable or disable the intermediate 3-DOF rail phase
637+ self .use_udot_rail2 = use_udot_rail2
629638
630639 # Controller initialization
631640 self .__init_controllers ()
@@ -1031,8 +1040,19 @@ def __check_simulation_events(self, phase, phase_index, node_index):
10311040 bool
10321041 True if an event occurred and the simulation should break.
10331042 """
1043+ # Check for the first time the rocket is between the two rail buttons
1044+ # (tip-off analysis). Optionally inserts the intermediate 3-DOF
1045+ # `udot_rail2` phase; see __handle_between_rails_event.
1046+ if len (self .between_rails_state ) == 1 and (
1047+ self .y_sol [0 ] ** 2
1048+ + self .y_sol [1 ] ** 2
1049+ + (self .y_sol [2 ] - self .env .elevation ) ** 2
1050+ >= self .effective_2rl ** 2
1051+ ):
1052+ if self .__handle_between_rails_event (phase , phase_index , node_index ):
1053+ return True
10341054 # Check for first out of rail event
1035- if len (self .out_of_rail_state ) == 1 and (
1055+ elif len (self .out_of_rail_state ) == 1 and (
10361056 self .y_sol [0 ] ** 2
10371057 + self .y_sol [1 ] ** 2
10381058 + (self .y_sol [2 ] - self .env .elevation ) ** 2
@@ -1127,6 +1147,47 @@ def __handle_out_of_rail_event(self, phase, phase_index, node_index):
11271147 phase .solver .status = "finished"
11281148 return True
11291149
1150+ def __handle_between_rails_event (self , phase , phase_index , node_index ):
1151+ """Handle the intermediate rail phase (tip-off analysis).
1152+
1153+ Records the state at which the rocket first reaches ``effective_2rl``
1154+ and, when ``use_udot_rail2`` is enabled, inserts the 3-DOF
1155+ ``udot_rail2`` flight phase.
1156+
1157+ Parameters
1158+ ----------
1159+ phase : FlightPhase
1160+ The current flight phase.
1161+ phase_index : int
1162+ The index of the current phase.
1163+ node_index : int
1164+ The index of the current node.
1165+
1166+ Returns
1167+ -------
1168+ bool
1169+ True if a new flight phase was inserted (simulation should break),
1170+ False otherwise.
1171+ """
1172+ self .between_rails_time = self .t
1173+ self .between_rails_time_index = len (self .solution ) - 1
1174+ self .between_rails_state = self .y_sol
1175+ # Optionally insert the udot_rail2 3-DOF phase. If disabled, the solver
1176+ # remains in the current phase and will transition to generalized
1177+ # dynamics at the upper button exit as before.
1178+ if self .use_udot_rail2 :
1179+ self .flight_phases .add_phase (
1180+ self .t ,
1181+ self .udot_rail2 ,
1182+ index = phase_index + 1 ,
1183+ )
1184+ # Prepare to leave loops and start new flight phase
1185+ phase .time_nodes .flush_after (node_index )
1186+ phase .time_nodes .add_node (self .t , [], [], [])
1187+ phase .solver .status = "finished"
1188+ return True
1189+ return False
1190+
11301191 def __handle_apogee_event (self , phase , phase_index , node_index ):
11311192 """Handle the apogee event.
11321193
@@ -1535,6 +1596,9 @@ def __init_solution_monitors(self):
15351596 self .out_of_rail_time = 0
15361597 self .out_of_rail_time_index = 0
15371598 self .out_of_rail_state = np .array ([0 ])
1599+ self .between_rails_state = np .array ([0 ])
1600+ self .between_rails_time = 0
1601+ self .between_rails_time_index = 0
15381602 self .apogee_state = np .array ([0 ])
15391603 self .apogee = 0
15401604 self .apogee_time = 0
@@ -1576,6 +1640,18 @@ def __init_flight_state(self):
15761640 e0_init , e1_init , e2_init , e3_init = euler313_to_quaternions (
15771641 self .phi_init , self .theta_init , self .psi_init
15781642 )
1643+
1644+ K_init = Matrix .transformation ([e0_init , e1_init , e2_init , e3_init ])
1645+
1646+ # Body axis pointing along rocket symmetry (body z-axis)
1647+ body_axis = Vector ([0 , 0 , 1 ])
1648+
1649+ # Attitude vector in inertial frame
1650+ attitude_vec = K_init @ body_axis
1651+
1652+ # Unit vector (normalize)
1653+ self .attitude_unit = attitude_vec / abs (attitude_vec )
1654+
15791655 # Store initial conditions
15801656 self .initial_solution = [
15811657 self .t_initial ,
@@ -1603,6 +1679,10 @@ def __init_flight_state(self):
16031679 self .out_of_rail_state = self .initial_solution [1 :]
16041680 self .out_of_rail_time = self .initial_solution [0 ]
16051681 self .out_of_rail_time_index = 0
1682+ # save out of rail 2 state and time with the same data as out of rail
1683+ self .between_rails_state = self .initial_solution [1 :]
1684+ self .between_rails_time = self .initial_solution [0 ]
1685+ self .between_rails_time_index = 0
16061686 # Set initial derivative for 6-DOF flight phase
16071687 self .initial_derivative = self .u_dot_generalized
16081688 else :
@@ -1611,6 +1691,10 @@ def __init_flight_state(self):
16111691 self .out_of_rail_state = self .initial_solution [1 :]
16121692 self .out_of_rail_time = self .initial_solution [0 ]
16131693 self .out_of_rail_time_index = 0
1694+ # save out of rail 2 state and time with the same data as out of rail
1695+ self .between_rails_state = self .initial_solution [1 :]
1696+ self .between_rails_time = self .initial_solution [0 ]
1697+ self .between_rails_time_index = 0
16141698 self .t_initial = self .initial_solution [0 ]
16151699 self .initial_derivative = self .u_dot_generalized
16161700 if self ._controllers or self .sensors :
@@ -1879,7 +1963,7 @@ def udot_rail1(self, t, u, post_processing=False):
18791963 return [vx , vy , vz , ax , ay , az , 0 , 0 , 0 , 0 , 0 , 0 , 0 ]
18801964
18811965 def udot_rail2 (self , t , u , post_processing = False ): # pragma: no cover
1882- """[Still not implemented ] Calculates derivative of u state vector with
1966+ """[WIP ] Calculates derivative of u state vector with
18831967 respect to time when rocket is flying in 3 DOF motion in the rail.
18841968
18851969 Parameters
@@ -1899,8 +1983,184 @@ def udot_rail2(self, t, u, post_processing=False): # pragma: no cover
18991983 State vector defined by u_dot = [vx, vy, vz, ax, ay, az,
19001984 e0dot, e1dot, e2dot, e3dot, alpha1, alpha2, alpha3].
19011985 """
1902- # Hey! We will finish this function later, now we just can use u_dot
1903- return self .u_dot_generalized (t , u , post_processing = post_processing )
1986+
1987+ # Retrieve integration data
1988+ _ , _ , z , vx , vy , vz , e0 , e1 , e2 , e3 , omega1 , omega2 , omega3 = u
1989+
1990+ # Create necessary vectors
1991+ # r = Vector([x, y, z]) # CDM position vector
1992+ v = Vector ([vx , vy , vz ]) # CDM velocity vector
1993+ e = [e0 , e1 , e2 , e3 ] # Euler parameters/quaternions
1994+ w = Vector ([omega1 , omega2 , omega3 ]) # Angular velocity vector
1995+
1996+ # Retrieve necessary quantities
1997+ ## Rocket mass
1998+ total_mass = self .rocket .total_mass .get_value_opt (t )
1999+ total_mass_dot = self .rocket .total_mass_flow_rate .get_value_opt (t )
2000+ total_mass_ddot = self .rocket .total_mass_flow_rate .differentiate_complex_step (t )
2001+ ## CM position vector and time derivatives relative to CDM in body frame
2002+ r_CM_z = self .rocket .com_to_cdm_function
2003+ r_CM_t = r_CM_z .get_value_opt (t )
2004+ r_CM = Vector ([0 , 0 , r_CM_t ])
2005+ r_CM_dot = Vector ([0 , 0 , r_CM_z .differentiate_complex_step (t )])
2006+ r_CM_ddot = Vector ([0 , 0 , r_CM_z .differentiate (t , order = 2 )])
2007+ ## Nozzle position vector
2008+ r_NOZ = Vector ([0 , 0 , self .rocket .nozzle_to_cdm ])
2009+ ## Nozzle gyration tensor
2010+ S_nozzle = self .rocket .nozzle_gyration_tensor
2011+ ## Inertia tensor
2012+ inertia_tensor = self .rocket .get_inertia_tensor_at_time (t )
2013+ ## Inertia tensor time derivative in the body frame
2014+ I_dot = self .rocket .get_inertia_tensor_derivative_at_time (t )
2015+
2016+ # Calculate the Inertia tensor relative to CM
2017+ H = (r_CM .cross_matrix @ - r_CM .cross_matrix ) * total_mass
2018+ I_CM = inertia_tensor - H
2019+
2020+ # Prepare transformation matrices
2021+ K = Matrix .transformation (e )
2022+ Kt = K .transpose
2023+
2024+ # Compute aerodynamic forces and moments
2025+ R1 , R2 , R3 , M1 , M2 , M3 = 0 , 0 , 0 , 0 , 0 , 0
2026+
2027+ ## Drag force
2028+ rho = self .env .density .get_value_opt (z )
2029+ wind_velocity_x = self .env .wind_velocity_x .get_value_opt (z )
2030+ wind_velocity_y = self .env .wind_velocity_y .get_value_opt (z )
2031+ wind_velocity = Vector ([wind_velocity_x , wind_velocity_y , 0 ])
2032+ free_stream_speed = abs ((wind_velocity - Vector (v )))
2033+ speed_of_sound = self .env .speed_of_sound .get_value_opt (z )
2034+ free_stream_mach = free_stream_speed / speed_of_sound
2035+
2036+ if self .rocket .motor .burn_start_time < t < self .rocket .motor .burn_out_time :
2037+ pressure = self .env .pressure .get_value_opt (z )
2038+ net_thrust = max (
2039+ self .rocket .motor .thrust .get_value_opt (t )
2040+ + self .rocket .motor .pressure_thrust (pressure ),
2041+ 0 ,
2042+ )
2043+ drag_coeff = self .rocket .power_on_drag .get_value_opt (free_stream_mach )
2044+ else :
2045+ net_thrust = 0
2046+ drag_coeff = self .rocket .power_off_drag .get_value_opt (free_stream_mach )
2047+ R3 += - 0.5 * rho * (free_stream_speed ** 2 ) * self .rocket .area * drag_coeff
2048+ # Get rocket velocity in body frame
2049+ velocity_in_body_frame = Kt @ v
2050+ # Calculate lift and moment for each component of the rocket
2051+ for aero_surface , _ in self .rocket .aerodynamic_surfaces :
2052+ # Component cp relative to CDM in body frame
2053+ comp_cp = self .rocket .surfaces_cp_to_cdm [aero_surface ]
2054+ # Component absolute velocity in body frame
2055+ comp_vb = velocity_in_body_frame + (w ^ comp_cp )
2056+ # Wind velocity at component altitude
2057+ comp_z = z + (K @ comp_cp ).z
2058+ comp_wind_vx = self .env .wind_velocity_x .get_value_opt (comp_z )
2059+ comp_wind_vy = self .env .wind_velocity_y .get_value_opt (comp_z )
2060+ # Component freestream velocity in body frame
2061+ comp_wind_vb = Kt @ Vector ([comp_wind_vx , comp_wind_vy , 0 ])
2062+ comp_stream_velocity = comp_wind_vb - comp_vb
2063+ comp_stream_speed = abs (comp_stream_velocity )
2064+ comp_stream_mach = comp_stream_speed / speed_of_sound
2065+ # Reynolds at component altitude
2066+ # TODO: Reynolds is only used in generic surfaces. This calculation
2067+ # should be moved to the surface class for efficiency
2068+ comp_reynolds = (
2069+ self .env .density .get_value_opt (comp_z )
2070+ * comp_stream_speed
2071+ * aero_surface .reference_length
2072+ / self .env .dynamic_viscosity .get_value_opt (comp_z )
2073+ )
2074+ # Forces and moments
2075+ X , Y , Z , M , N , L = aero_surface .compute_forces_and_moments (
2076+ comp_stream_velocity ,
2077+ comp_stream_speed ,
2078+ comp_stream_mach ,
2079+ rho ,
2080+ comp_cp ,
2081+ w ,
2082+ comp_reynolds ,
2083+ )
2084+ R1 += X
2085+ R2 += Y
2086+ R3 += Z
2087+ M1 += M
2088+ M2 += N
2089+ M3 += L
2090+
2091+ # Off center moment
2092+ M1 += (
2093+ self .rocket .cp_eccentricity_y * R3
2094+ + self .rocket .thrust_eccentricity_y * net_thrust
2095+ )
2096+ M2 -= (
2097+ self .rocket .cp_eccentricity_x * R3
2098+ + self .rocket .thrust_eccentricity_x * net_thrust
2099+ )
2100+ M3 += self .rocket .cp_eccentricity_x * R2 - self .rocket .cp_eccentricity_y * R1
2101+
2102+ weight_in_body_frame = Kt @ Vector (
2103+ [0 , 0 , - total_mass * self .env .gravity .get_value_opt (z )]
2104+ )
2105+
2106+ T00 = total_mass * r_CM
2107+ T03 = 2 * total_mass_dot * (r_NOZ - r_CM ) - 2 * total_mass * r_CM_dot
2108+ T04 = (
2109+ Vector ([0 , 0 , net_thrust ])
2110+ - total_mass * r_CM_ddot
2111+ - 2 * total_mass_dot * r_CM_dot
2112+ + total_mass_ddot * (r_NOZ - r_CM )
2113+ )
2114+ T05 = total_mass_dot * S_nozzle - I_dot
2115+
2116+ T20 = (
2117+ ((w ^ T00 ) ^ w )
2118+ + (w ^ T03 )
2119+ + T04
2120+ + weight_in_body_frame
2121+ + Vector ([R1 , R2 , R3 ])
2122+ )
2123+
2124+ T21 = (
2125+ ((inertia_tensor @ w ) ^ w )
2126+ + T05 @ w
2127+ - (weight_in_body_frame ^ r_CM )
2128+ + Vector ([M1 , M2 , M3 ])
2129+ )
2130+
2131+ # Angular velocity derivative
2132+ w_dot = I_CM .inverse @ (T21 + (T20 ^ r_CM ))
2133+ # Enforce zero roll acceleration for 3-DOF rail motion by creating
2134+ # a new Vector with the third component set to zero instead of
2135+ # attempting item assignment on the Vector type.
2136+ w_dot = Vector ([w_dot [0 ], w_dot [1 ], 0.0 ])
2137+
2138+ # Euler parameters derivative
2139+ e_dot = [
2140+ 0.5 * (- omega1 * e1 - omega2 * e2 ), # - omega3 * e3),
2141+ 0.5 * (omega1 * e0 - omega2 * e3 ), # omega3 * e2
2142+ 0.5 * (omega2 * e0 + omega1 * e3 ), # - omega3 * e1
2143+ 0.5 * (omega2 * e1 - omega1 * e2 ), # +omega3 * e0
2144+ ]
2145+
2146+ # Velocity vector derivative + Coriolis acceleration
2147+ w_earth = Vector (self .env .earth_rotation_vector )
2148+ v_dot = K @ (T20 / total_mass - (r_CM ^ w_dot )) - 2 * (w_earth ^ v )
2149+
2150+ # Position vector derivative: projection of velocity along the rail
2151+ rail = self .attitude_unit # unit vector inertial frame
2152+ velocity_vec = Vector ([vx , vy , vz ])
2153+ r_dot = rail * (velocity_vec @ rail )
2154+
2155+ # Create u_dot
2156+ u_dot = [* r_dot , * v_dot , * e_dot , * w_dot ]
2157+
2158+ if post_processing :
2159+ self .__post_processed_variables .append (
2160+ [t , * v_dot , * w_dot , R1 , R2 , R3 , M1 , M2 , M3 , net_thrust ]
2161+ )
2162+
2163+ return u_dot
19042164
19052165 def u_dot (self , t , u , post_processing = False ): # pylint: disable=too-many-locals,too-many-statements
19062166 """Calculates derivative of u state vector with respect to time
@@ -4212,6 +4472,7 @@ def to_dict(self, **kwargs):
42124472 "time" : self .time ,
42134473 "out_of_rail_velocity" : self .out_of_rail_velocity ,
42144474 "out_of_rail_state" : self .out_of_rail_state ,
4475+ "between_rails_state" : self .between_rails_state ,
42154476 "apogee_x" : self .apogee_x ,
42164477 "apogee_y" : self .apogee_y ,
42174478 "apogee_state" : self .apogee_state ,
0 commit comments