@@ -492,6 +492,7 @@ def __init__( # pylint: disable=too-many-arguments,too-many-statements
492492 equations_of_motion = "standard" ,
493493 ode_solver = "LSODA" ,
494494 simulation_mode = "6 DOF" ,
495+ weathercock_coeff = 1.0 ,
495496 ):
496497 """Run a trajectory simulation.
497498
@@ -575,6 +576,13 @@ def __init__( # pylint: disable=too-many-arguments,too-many-statements
575576 A custom ``scipy.integrate.OdeSolver`` can be passed as well.
576577 For more information on the integration methods, see the scipy
577578 documentation [1]_.
579+ weathercock_coeff : float, optional
580+ Coefficient that controls the rate at which the rocket's body axis
581+ aligns with the relative wind direction in 3-DOF simulations.
582+ A higher value means faster alignment (quasi-static weathercocking).
583+ This parameter is only used when simulation_mode is '3 DOF'.
584+ Default is 1.0, which provides moderate alignment. Set to 0 to
585+ disable weathercocking (fixed attitude).
578586
579587
580588 Returns
@@ -606,7 +614,8 @@ def __init__( # pylint: disable=too-many-arguments,too-many-statements
606614 self .equations_of_motion = equations_of_motion
607615 self .simulation_mode = simulation_mode
608616 self .ode_solver = ode_solver
609-
617+ self .weathercock_coeff = weathercock_coeff
618+
610619 # Controller initialization
611620 self .__init_controllers ()
612621
@@ -1793,7 +1802,8 @@ def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals
17931802 def u_dot_generalized_3dof (self , t , u , post_processing = False ):
17941803 """Calculates derivative of u state vector with respect to time when the
17951804 rocket is flying in 3 DOF motion in space and significant mass variation
1796- effects exist.
1805+ effects exist.Includes a weathercocking model that evolves the body axis
1806+ direction toward the relative wind direction.
17971807
17981808 Parameters
17991809 ----------
@@ -1898,7 +1908,70 @@ def u_dot_generalized_3dof(self, t, u, post_processing=False):
18981908 e_dot = [0 , 0 , 0 , 0 ] # Euler derivatives unused in 3DOF
18991909 w_dot = [0 , 0 , 0 ] # No angular dynamics in 3DOF
19001910 r_dot = [vx , vy , vz ]
1911+ # Weathercocking: evolve body axis direction toward relative wind
1912+ # The body z-axis (attitude vector) should align with -freestream_velocity
1913+ if self .weathercock_coeff > 0 and free_stream_speed > 1e-6 :
1914+ # Current body z-axis in inertial frame (attitude vector)
1915+ # From rotation matrix: column 3 gives the body z-axis in inertial frame
1916+ body_z_inertial = Vector (
1917+ [
1918+ 2 * (e1 * e3 + e0 * e2 ),
1919+ 2 * (e2 * e3 - e0 * e1 ),
1920+ 1 - 2 * (e1 ** 2 + e2 ** 2 ),
1921+ ]
1922+ )
1923+
1924+ # Desired direction: opposite of freestream velocity (into the wind)
1925+ # This is the direction the rocket nose should point
1926+ # Division by free_stream_speed ensures the result is a unit vector
1927+ desired_direction = - free_stream_velocity / free_stream_speed
1928+
1929+ # Compute rotation axis (cross product of current and desired)
1930+ rotation_axis = body_z_inertial ^ desired_direction
1931+ rotation_axis_mag = abs (rotation_axis )
1932+
1933+ if rotation_axis_mag > 1e-8 :
1934+ # Normalize rotation axis
1935+ rotation_axis = rotation_axis / rotation_axis_mag
19011936
1937+ # The magnitude of the cross product of two unit vectors equals
1938+ # the sine of the angle between them
1939+ sin_angle = rotation_axis_mag
1940+
1941+ # Clamp sin_angle to valid range
1942+ sin_angle = min (1.0 , max (- 1.0 , sin_angle ))
1943+
1944+ # Angular velocity magnitude proportional to misalignment angle
1945+ # Using sin(angle) as approximation for small angles: sin(theta) ≈ theta
1946+ omega_mag = self .weathercock_coeff * sin_angle
1947+
1948+ # Angular velocity in inertial frame
1949+ omega_inertial = rotation_axis * omega_mag
1950+
1951+ # Transform angular velocity to body frame
1952+ omega_body = Kt @ omega_inertial
1953+
1954+ # Quaternion derivative from angular velocity in body frame
1955+ omega1_wc , omega2_wc , omega3_wc = (
1956+ omega_body .x ,
1957+ omega_body .y ,
1958+ omega_body .z ,
1959+ )
1960+ e0_dot = 0.5 * (- omega1_wc * e1 - omega2_wc * e2 - omega3_wc * e3 )
1961+ e1_dot = 0.5 * (omega1_wc * e0 + omega3_wc * e2 - omega2_wc * e3 )
1962+ e2_dot = 0.5 * (omega2_wc * e0 - omega3_wc * e1 + omega1_wc * e3 )
1963+ e3_dot = 0.5 * (omega3_wc * e0 + omega2_wc * e1 - omega1_wc * e2 )
1964+ e_dot = [e0_dot , e1_dot , e2_dot , e3_dot ]
1965+ w_dot = [0 , 0 , 0 ] # No angular acceleration in 3DOF model
1966+ else :
1967+ # Already aligned or anti-aligned
1968+ e_dot = [0 , 0 , 0 , 0 ]
1969+ w_dot = [0 , 0 , 0 ]
1970+ else :
1971+ # No weathercocking or negligible freestream speed
1972+ e_dot = [0 , 0 , 0 , 0 ]
1973+ w_dot = [0 , 0 , 0 ]
1974+
19021975 u_dot = [* r_dot , * v_dot , * e_dot , * w_dot ]
19031976
19041977 if post_processing :
0 commit comments