import numpy as np
import matplotlib.pyplot as plt
import scipy
import os
from pymycar.files import prepare_simulation, append_results_to_file
from pymycar.Logger.library_versions import set_logger, log_library_versions, log_system_info, log_end_analysis#, log_model_information
[docs]
def solver(system, time_points, initial_conditions):
"""
Solve the differential equations.
Returns:
--------
np.ndarray
Solution array.
"""
solution = scipy.integrate.solve_ivp(system,
(np.min(time_points), np.max(time_points)),
initial_conditions,
method = 'RK45',
t_eval = time_points,
max_step = np.min(np.diff(time_points)))
return solution
# -------------------- ^
# | | | zs
# | ms | ---
# | |
# --------------------
# \ |
# k1 / |_| c1
# \ |
# --------------- ^
# | | | zu
# | mu | ---
# | |
# ---------------
# \ |
# kw1 / |_| cw1
# \ |
# ---------
# \ /
# * __ __
# ______________/ \ / \_________
# \__/
[docs]
class VerticalQuarterCar:
"""
Simulate a vertical quarter car model and save results to a text file.
Parameters:
----------
custom_excitation : function
Custom excitation function.
time_points : array-like
Time points for simulation.
Attributes:
----------
number_dofs : int
Number of degrees of freedom (constant).
Methods:
--------
get_mass_matrix()
get_damper_matrix()
get_stiffness_matrix()
get_F_vector(t)
system(y, t)
solve()
save_2_text_file()
plot_results()
"""
number_dofs = 2
def __init__(self, mycar, quarter_part="right_front", result_folder_name="results", path = None):
"""
Initialize the VerticalQuarterCar object.
Parameters:
----------
custom_excitation : function
Custom excitation function.
time_points : array-like
Time points for simulation.
"""
self.result_folder_name = result_folder_name
if path is None:
path = os.getcwd()
prepare_simulation(path, self.result_folder_name)
# logger_suspension_kinematics(data, max_height_increase, max_height_decrease, height_step, save_to_txt, result_folder_name, path)
logger = set_logger(self.result_folder_name)
log_system_info(logger) # log system imformation
log_library_versions(logger) # log Library versions
# Parameters
self.ms = mycar.chassis.mass/4
if quarter_part == "right_front":
self.mu = mycar.right_front_wheel.mass
self.c1 = mycar.right_front_suspension.damper
self.k1 = mycar.right_front_suspension.stiffness
self.cw1 = mycar.right_front_wheel.radial_damping
self.kw1 = mycar.right_front_wheel.radial_stiffness
elif quarter_part == "left_front":
self.mu = mycar.left_front_wheel.mass
self.c1 = mycar.left_front_suspension.damper
self.k1 = mycar.left_front_suspension.stiffness
self.cw1 = mycar.left_front_wheel.radial_damping
self.kw1 = mycar.left_front_wheel.radial_stiffness
elif quarter_part == "right_rear":
self.mu = mycar.right_rear_wheel.mass
self.c1 = mycar.right_rear_suspension.damper
self.k1 = mycar.right_rear_suspension.stiffness
self.cw1 = mycar.right_rear_wheel.radial_damping
self.kw1 = mycar.right_rear_wheel.radial_stiffness
elif quarter_part == "left_rear":
self.mu = mycar.left_rear_wheel.mass
self.c1 = mycar.left_rear_suspension.damper
self.k1 = mycar.left_rear_suspension.stiffness
self.cw1 = mycar.left_rear_wheel.radial_damping
self.kw1 = mycar.left_rear_wheel.radial_stiffness
else:
print("Invalid quarter part. Choose between 'right_front', 'left_front', 'right_rear', 'left_rear'")
# Mass, damping, and stiffness matrices
self.M = self.get_mass_matrix()
self.C = self.get_damper_matrix()
self.K = self.get_stiffness_matrix()
mycar.chassis.x = 0
mycar.chassis.y = 0
mycar.chassis.z = 0
# Initial conditions:
self.zs_time0 = 0
self.zs_dot_time0 = 0
self.zu_time0 = 0
self.zu_dot_time0 = 0
self.initial_conditions = np.array([self.zs_time0,
self.zs_dot_time0,
self.zu_time0,
self.zu_dot_time0])
self.time_points = None
# Wheel excitation
self.wheel_excitation = None
self.wheel_excitation_dot = None
# Results variables
self.zs = None
self.zu = None
self.zu_dot = None
self.zs_dot = None
[docs]
def get_mass_matrix(self):
"""
Get the mass matrix.
Returns:
-------
np.ndarray
Mass matrix.
"""
M = np.zeros([self.number_dofs, self.number_dofs])
M[0, 0] = self.ms
M[1, 1] = self.mu
return M
[docs]
def get_damper_matrix(self):
"""
Get the damper matrix.
Returns:
-------
np.ndarray
Damper matrix.
"""
C = np.zeros([self.number_dofs, self.number_dofs])
C[0, 0] = self.c1
C[1, 0] = -self.c1
C[1, 1] = self.c1 + self.cw1
C[0, 1] = -self.c1
return C
[docs]
def get_stiffness_matrix(self):
"""
Get the stiffness matrix.
Returns:
-------
np.ndarray
Stiffness matrix.
"""
K = np.zeros([self.number_dofs, self.number_dofs])
K[0, 0] = self.k1
K[1, 0] = -self.k1
K[1, 1] = self.k1 + self.kw1
K[0, 1] = -self.k1
return K
[docs]
def natural_frequencies(self):
#self.eigenvalues, self.eigenvectors = np.linalg.eig(np.dot(np.linalg.inv(self.M), self.K))
self.eigenvalues, self.eigenvectors = scipy.linalg.eig(self.K, self.M)
self.natural_frequencies_hz = np.sqrt(self.eigenvalues) / (2 * np.pi)
[docs]
def get_F_vector(self, t):
"""
Get the force vector at time t.
Parameters:
----------
t : float
Current time.
Returns:
-------
np.ndarray
Force vector.
"""
F = np.zeros(self.number_dofs)
F[1] = self.kw1 * self.wheel_excitation(t)
return F
[docs]
def system(self, t, y):
"""
System of differential equations.
Parameters:
----------
y : np.ndarray
State vector.
t : float
Current time.
Returns:
-------
np.ndarray
Derivative of the state vector.
"""
q, q_dot = np.split(y, 2)
F = self.get_F_vector(t)
q_dotdot = np.linalg.solve(self.M, F - np.dot(self.C, q_dot) - np.dot(self.K, q))
return np.concatenate((q_dot, q_dotdot))
[docs]
def solve(self):
"""
Solve the differential equations.
Returns:
-------
np.ndarray
Solution array.
"""
solution = solver(self.system, self.time_points, self.initial_conditions)
self.zs = solution.y[0]
self.zu = solution.y[1]
self.zu_dot = solution.y[2]
self.zs_dot = solution.y[3]
self.time = solution.t
self.save_2_text_file("data.txt")
return solution
[docs]
def save_2_text_file(self, filename='data.txt'):
"""
Save results to a text file.
Parameters:
----------
filename : str, optional
Name of the output text file (default is 'data.txt').
"""
# Combine arrays into one 2D array
combined_array = np.column_stack((self.time, self.zs, self.zu, self.zu_dot, self.zs_dot))
# Add a header
header = "time, zs, zu, zu_dot, zs_dot "
# Save to a text file
np.savetxt(os.path.join(self.result_folder_name, filename), combined_array, delimiter=' ', header=header)
[docs]
class VerticalHalfCar:
number_dofs = 4
def __init__(self, mycar,custom_excitation, time_points, part="front"):
self.time_points = time_points
self.ms = mycar.chassis.mass/2
self.Is = mycar.chassis.inertia_y
self.mu1 = mycar.right_front_wheel.mass
self.mu2 = mycar.left_front_wheel.mass
self.c1 = mycar.right_front_suspension.damper
self.c2 = mycar.left_front_suspension.damper
self.cw1 = mycar.right_front_wheel.radial_damping
self.cw2 = mycar.left_front_wheel.radial_damping
self.k1 = mycar.right_front_suspension.stiffness
self.k2 = mycar.left_front_suspension.stiffness
self.kw1 = mycar.right_front_wheel.radial_stiffness
self.kw2 = mycar.left_front_wheel.radial_stiffness
self.l1 = mycar.chassis.front_track/2
self.l2 = mycar.chassis.front_track/2
self.M = self.get_mass_matrix()
self.C = self.get_damper_matrix()
self.K = self.get_stiffness_matrix()
self.wheel_excitation_1 = custom_excitation
self.wheel_excitation_2 = custom_excitation
self.wheel_excitation = custom_excitation
# Initial conditions:
self.z_s_initial = 0
self.z_s_dot_initial = 0
self.theta_initial = 0
self.theta_dot_initial = 0
self.z_u1_initial = 0
self.z_u1_dot_initial = 0
self.z_u2_initial = 0
self.z_u2_dot_initial = 0
self.initial_conditions = np.array([
self.z_s_initial,
self.z_s_dot_initial,
self.theta_initial,
self.theta_dot_initial,
self.z_u1_initial,
self.z_u1_dot_initial,
self.z_u2_initial,
self.z_u2_dot_initial
])
[docs]
def get_mass_matrix(self):
M = np.zeros([self.number_dofs, self.number_dofs])
M[0, 0] = self.ms
M[1, 1] = self.Is
M[2, 2] = self.mu1
M[3, 3] = self.mu2
return M
[docs]
def get_damper_matrix(self):
C = np.zeros([self.number_dofs, self.number_dofs])
C[0, 0] = self.c1 + self.c2
C[0, 1] = -self.c1*self.l1 + self.c2*self.l2
C[0, 2] = -self.c1
C[0, 3] = -self.c2
C[1, 0] = C[0, 1]
C[1, 1] = self.c1*self.l1**2 + self.c2*self.l2**2
C[1, 2] = self.c1*self.l1
C[1, 3] =-self.c2*self.l2
C[2, 0] = C[0, 2]
C[2, 1] = C[1, 2]
C[2, 2] = self.c1 + self.cw1
C[2, 3] = 0.0
C[3, 0] = C[0, 3]
C[3, 1] = C[1, 3]
C[3, 2] = C[2, 3]
C[3, 3] = self.c2 + self.cw2
return C
[docs]
def get_stiffness_matrix(self):
K = np.zeros([self.number_dofs, self.number_dofs])
K[0, 0] = self.k1 + self.k2
K[0, 1] = -self.k1*self.l1 + self.k2*self.l2
K[0, 2] = -self.k1
K[0, 3] = -self.k2
K[1, 0] = K[0, 1]
K[1, 1] = self.k1*self.l1**2 + self.k2*self.l2**2
K[1, 2] = self.k1*self.l1
K[1, 3] =-self.k2*self.l2
K[2, 0] = K[0, 2]
K[2, 1] = K[1, 2]
K[2, 2] = self.k1 + self.kw1
K[2, 3] = 0.0
K[3, 0] = K[0, 3]
K[3, 1] = K[1, 3]
K[3, 2] = K[2, 3]
K[3, 3] = self.k2 + self.kw2
return K
[docs]
def get_F_vector(self, t):
F = np.zeros(self.number_dofs)
F[2] = self.wheel_excitation(t)
F[3] = self.wheel_excitation(t)
return F
[docs]
def system(self, y, t):
q, q_dot = np.split(y, 2)
F = self.get_F_vector(t)
q_dotdot = np.linalg.solve(self.M, F - np.dot(self.C, q_dot) - np.dot(self.K, q))
return np.concatenate((q_dot, q_dotdot))
[docs]
def solve(self):
solution = scipy.integrate.odeint(self.system, self.initial_conditions, self.time_points)
self.Zs = solution[:,0]
self.Ztheta = solution[:,1]
self.Zu1 = solution[:, 2]
self.Zu2 = solution[:, 3]
self.Zs_dot = solution[:,4]
self.Ztheta_dot = solution[:,5]
self.Zu1_dot = solution[:, 6]
self.Zu2_dot = solution[:, 7]
return solution
[docs]
class VerticalCar:
number_dofs = 7
def __init__(self,mycar, custom_excitation, time_points):
self.wheel_excitation = custom_excitation
self.time_points = time_points
self.ms = mycar.chassis.mass
self.ixx = mycar.chassis.inertia_x
self.iyy = mycar.chassis.inertia_y
self.mu1 = mycar.right_front_wheel.mass
self.mu2 = mycar.left_front_wheel.mass
self.mu3 = mycar.right_rear_wheel.mass
self.mu4 = mycar.left_rear_wheel.mass
self.c1 = mycar.right_front_suspension.damper
self.c2 = mycar.left_front_suspension.damper
self.c3 = mycar.right_rear_suspension.damper
self.c4 = mycar.left_rear_suspension.damper
self.cw1 = mycar.right_front_wheel.radial_damping
self.cw2 = mycar.left_front_wheel.radial_damping
self.cw3 = mycar.right_rear_wheel.radial_damping
self.cw4 = mycar.left_rear_wheel.radial_damping
self.k1 = mycar.right_front_suspension.stiffness
self.k2 = mycar.left_front_suspension.stiffness
self.k3 = mycar.right_rear_suspension.stiffness
self.k4 = mycar.left_rear_suspension.stiffness
self.kw1 = mycar.right_front_wheel.radial_stiffness
self.kw2 = mycar.left_front_wheel.radial_stiffness
self.kw3 = mycar.right_rear_wheel.radial_stiffness
self.kw4 = mycar.left_rear_wheel.radial_stiffness
self.lf = mycar.chassis.front_axle_to_com
self.lr = mycar.chassis.rear_axle_to_com
self.t1 = mycar.chassis.front_track/2
self.t2 = mycar.chassis.front_track/2
self.t3 = mycar.chassis.rear_track/2
self.t4 = mycar.chassis.rear_track/2
self.M = self.get_mass_matrix()
self.C = self.get_damper_matrix()
self.K = self.get_stiffness_matrix()
self.wheel_excitation_1 = custom_excitation
self.wheel_excitation_2 = custom_excitation
self.wheel_excitation_3 = custom_excitation
self.wheel_excitation_4 = custom_excitation
# Initial conditions:
self.z_s_initial = 0
self.z_s_dot_initial = 0
self.alpha_initial = 0
self.alpha_dot_initial = 0
self.beta_initial = 0
self.beta_dot_initial = 0
self.z_u1_initial = 0
self.z_u1_dot_initial = 0
self.z_u2_initial = 0
self.z_u2_dot_initial = 0
self.z_u3_initial = 0
self.z_u3_dot_initial = 0
self.z_u4_initial = 0
self.z_u4_dot_initial = 0
self.initial_conditions = np.array([
self.z_s_initial,
self.z_s_dot_initial,
self.alpha_initial,
self.alpha_dot_initial,
self.beta_initial,
self.beta_dot_initial,
self.z_u1_initial,
self.z_u1_dot_initial,
self.z_u2_initial,
self.z_u2_dot_initial,
self.z_u3_initial,
self.z_u3_dot_initial,
self.z_u4_initial,
self.z_u4_dot_initial,
])
[docs]
def get_mass_matrix(self):
M = np.zeros([self.number_dofs, self.number_dofs])
M[0, 0] = self.ms
M[1, 1] = self.ixx
M[2, 2] = self.iyy
M[3, 3] = self.mu1
M[4, 4] = self.mu2
M[5, 5] = self.mu3
M[6, 6] = self.mu4
return M
[docs]
def get_damper_matrix(self):
C = np.zeros([self.number_dofs, self.number_dofs])
C[0, 0] = self.c1 + self.c2 + self.c3 + self.c4
C[0, 1] = self.c1*self.t1 - self.c2*self.t2 + self.c3*self.t3 - self.c4*self.t4
C[0, 2] = self.c1*self.lf + self.c2*self.lf - self.c3*self.lr - self.c4*self.lr
C[0, 3] = -self.c1
C[0, 4] = -self.c2
C[0, 5] = -self.c3
C[0, 6] = -self.c4
C[1, 0] = C[0, 1]
C[1, 1] = self.c1*self.t1**2 + self.c2*self.t2**2 + self.c3*self.t3**2 + self.c4*self.t4**2
C[1, 2] = self.c1*self.t1*self.lf + self.c2*self.t2*self.lf - self.c3*self.t3*self.lr - self.c4*self.t4*self.lr
C[1, 3] = -self.c1*self.t1
C[1, 4] = self.c2*self.t2
C[1, 5] = -self.c3*self.t3
C[1, 6] = self.c4*self.t4
C[2, 0] = C[0, 2]
C[2, 1] = C[1, 2]
C[2, 2] = self.c1*self.lf**2 + self.c2*self.lf**2 + self.c3*self.lr**2 + self.c4*self.lr**2
C[2, 3] = -self.c1*self.lf
C[2, 4] = -self.c2*self.lf
C[2, 5] = self.c3*self.lr
C[2, 6] = self.c4*self.lr
C[3, 0] = C[0, 3]
C[3, 1] = C[1, 3]
C[3, 2] = C[2, 3]
C[3, 3] = self.c1 + self.cw1
C[3, 4] = 0.0
C[3, 5] = 0.0
C[3, 6] = 0.0
C[4, 0] = C[0, 4]
C[4, 1] = C[1, 4]
C[4, 2] = C[2, 4]
C[4, 3] = C[3, 4]
C[4, 4] = self.c2 + self.cw2
C[4, 5] = 0.0
C[4, 6] = 0.0
C[5, 0] = C[0, 5]
C[5, 1] = C[1, 5]
C[5, 2] = C[2, 5]
C[5, 3] = C[3, 5]
C[5, 4] = C[4, 5]
C[5, 5] = self.c3 + self.cw3
C[5, 6] = 0.0
C[6, 0] = C[0, 6]
C[6, 1] = C[1, 6]
C[6, 2] = C[2, 6]
C[6, 3] = C[3, 6]
C[6, 4] = C[4, 6]
C[6, 5] = C[5, 6]
C[6, 6] = self.c4 + self.cw4
return C
[docs]
def get_stiffness_matrix(self):
K = np.zeros([self.number_dofs, self.number_dofs])
K[0, 0] = self.k1 + self.k2 + self.k3 + self.k4
K[0, 1] = self.k1*self.t1 - self.k2*self.t2 + self.k3*self.t3 - self.k4*self.t4
K[0, 2] = self.k1*self.lf + self.k2*self.lf - self.k3*self.lr - self.k4*self.lr
K[0, 3] = -self.k1
K[0, 4] = -self.k2
K[0, 5] = -self.k3
K[0, 6] = -self.k4
K[1, 0] = K[0, 1]
K[1, 1] = self.k1*self.t1**2 + self.k2*self.t2**2 + self.k3*self.t3**2 + self.k4*self.t4**2
K[1, 2] = self.k1*self.t1*self.lf + self.k2*self.t2*self.lf - self.k3*self.t3*self.lr - self.k4*self.t4*self.lr
K[1, 3] = -self.k1*self.t1
K[1, 4] = self.k2*self.t2
K[1, 5] = -self.k3*self.t3
K[1, 6] = self.k4*self.t4
K[2, 0] = K[0, 2]
K[2, 1] = K[1, 2]
K[2, 2] = self.k1*self.lf**2 + self.k2*self.lf**2 + self.k3*self.lr**2 + self.k4*self.lr**2
K[2, 3] = -self.k1*self.lf
K[2, 4] = -self.k2*self.lf
K[2, 5] = self.k3*self.lr
K[2, 6] = self.k4*self.lr
K[3, 0] = K[0, 3]
K[3, 1] = K[1, 3]
K[3, 2] = K[2, 3]
K[3, 3] = self.k1 + self.kw1
K[3, 4] = 0.0
K[3, 5] = 0.0
K[3, 6] = 0.0
K[4, 0] = K[0, 4]
K[4, 1] = K[1, 4]
K[4, 2] = K[2, 4]
K[4, 3] = K[3, 4]
K[4, 4] = self.k2 + self.kw2
K[4, 5] = 0.0
K[4, 6] = 0.0
K[5, 0] = K[0, 5]
K[5, 1] = K[1, 5]
K[5, 2] = K[2, 5]
K[5, 3] = K[3, 5]
K[5, 4] = K[4, 5]
K[5, 5] = self.k3 + self.kw3
K[5, 6] = 0.0
K[6, 0] = K[0, 6]
K[6, 1] = K[1, 6]
K[6, 2] = K[2, 6]
K[6, 3] = K[3, 6]
K[6, 4] = K[4, 6]
K[6, 5] = K[5, 6]
K[6, 6] = self.k4 + self.kw4
return K
[docs]
def get_F_vector(self, t):
F = np.zeros(self.number_dofs)
F[3] = self.wheel_excitation(t)
F[4] = self.wheel_excitation(t)
F[5] = self.wheel_excitation(t)
F[6] = self.wheel_excitation(t)
return F
[docs]
def system(self, y, t):
q, q_dot = np.split(y, 2)
F = self.get_F_vector(t)
q_dotdot = np.linalg.solve(self.M, F - np.dot(self.C, q_dot) - np.dot(self.K, q))
return np.concatenate((q_dot, q_dotdot))
[docs]
def solve(self):
solution = scipy.integrate.odeint(self.system, self.initial_conditions, self.time_points)
self.Zs = solution[:, 0]
self.alpha = solution[:, 1]
self.beta = solution[:, 2]
self.Zu1 = solution[:, 3]
self.Zu2 = solution[:, 4]
self.Zu3 = solution[:, 5]
self.Zu4 = solution[:, 6]
self.Zs_dot = solution[:, 7]
self.alpha_dot = solution[:, 8]
self.beta_dot = solution[:, 9]
self.Zu1_dot = solution[:, 10]
self.Zu2_dot = solution[:, 11]
self.Zu3_dot = solution[:, 12]
self.Zu4_dot = solution[:, 13]
return solution
[docs]
def plot_results(self):
fig, ax_q = plt.subplots()
ax_q.plot(self.time_points, self.Zs, 'k-', linewidth=2.0, label="Zs")
ax_q.plot(self.time_points, self.Zu1, 'g-', linewidth=2.0, label="Zu1")
ax_q.plot(self.time_points, self.Zu2, 'b--', linewidth=2.0, label="Zu2")
ax_q.plot(self.time_points, self.Zu3, 'y-', linewidth=2.0, label="Zu3")
ax_q.plot(self.time_points, self.Zu4, 'b--', linewidth=2.0, label="Zu4")
ax_q.grid(color='k', linestyle='-', linewidth=0.3)
ax_q.set_xlabel('time' )
ax_q.set_ylabel('displacement')
ax_q.set_title('Displacement')
ax_q.legend()
fig, ax_qa = plt.subplots()
ax_qa.plot(self.time_points, self.beta, 'r-', linewidth=2.0, label="theta")
ax_qa.plot(self.time_points, self.alpha, 'y-', linewidth=2.0, label="alpha")
ax_qa.set_xlabel('time' )
ax_qa.set_ylabel('angle')
ax_qa.set_title('angle')
ax_qa.legend()
fig, ax_q_dot = plt.subplots()
#ax_q_dot.plot(self.time_points, self.Zs_dot, 'r-', linewidth=2.0, label="Zs_dot")
ax_q_dot.plot(self.time_points, self.Zu1_dot, 'g-', linewidth=2.0, label="Zu1_dot")
ax_q_dot.plot(self.time_points, self.Zu2_dot, 'b--', linewidth=2.0, label="Zu2_dot")
ax_q_dot.plot(self.time_points, self.Zu3_dot, 'g-', linewidth=2.0, label="Zu1_dot")
ax_q_dot.plot(self.time_points, self.Zu4_dot, 'b--', linewidth=2.0, label="Zu2_dot")
#ax_q_dot.plot(self.time_points[:-1], np.diff(self.Zs)/np.diff(self.time_points), 'k--', linewidth=2.0, label="Zu_dot")
ax_q_dot.grid(color='k', linestyle='-', linewidth=0.3)
ax_q_dot.set_xlabel('time' )
ax_q_dot.set_ylabel('velocity')
ax_q_dot.set_title('Velocity')
ax_q_dot.legend()
plt.show()