python中学物理实验模拟:瞬间推力与摩擦力作用下的物体运动
python中学物理实验模拟:瞬间推力与摩擦力作用下的物体运动
下面程序通过图形用户界面允许用户设置物体的质量、推力、冲击时间和摩擦系数,模拟物体在推力作用下的运动过程,并实时显示物体的位置 - 时间曲线。用户可以通过 “推动” 按钮启动模拟,“归位” 按钮重置模拟。同时,程序考虑了摩擦系数为 0 的特殊情况,提供了相应的提示信息。
运行情况:
源码如下:
import tkinter as tk
from tkinter import ttk, messagebox
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.ticker import MultipleLocatorclass MotionSimulator:def __init__(self, root):self.root = rootself.root.title("瞬间推力与摩擦力作用下的物体运动")self.root.geometry("900x780") # 减小窗口宽度# 设置中文支持plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']plt.rcParams['axes.unicode_minus'] = False# 初始化物体和动画参数self.block_size = 40self.velocity = 0self.position = 0self.animation = Noneself.is_moving = Falseself.time_data = []self.position_data = []self.line = Noneself.animation_id = Noneself.zero_friction_warning_shown = False# 配置网格布局root.grid_columnconfigure(0, weight=1) # 控制面板root.grid_columnconfigure(1, weight=4) # 图表区域权重增加为4倍root.grid_rowconfigure(0, weight=3)root.grid_rowconfigure(1, weight=2)# 创建控制面板 - 减小宽度self.control_frame = tk.Frame(root, padx=8, pady=8, width=220, height=400)self.control_frame.grid(row=0, column=0, sticky="nsew")self.control_frame.grid_propagate(False)# 创建图表区域self.figure_frame = tk.Frame(root, padx=10, pady=10)self.figure_frame.grid(row=0, column=1, sticky="nsew")# 创建物体动画区域 - 增加高度self.bottom_frame = tk.Frame(root, padx=10, pady=10, height=250)self.bottom_frame.grid(row=1, column=0, columnspan=2, sticky="nsew")self.bottom_frame.grid_propagate(False)# 添加质量输入tk.Label(self.control_frame, text="质量 (kg):").pack(anchor=tk.W)self.mass_var = tk.StringVar(value="1.0")mass_entry = ttk.Entry(self.control_frame, textvariable=self.mass_var, width=8)mass_entry.pack(anchor=tk.W, pady=(0, 5))# 添加推力输入tk.Label(self.control_frame, text="推力 (N):").pack(anchor=tk.W)self.force_var = tk.StringVar(value="50.0")force_entry = ttk.Entry(self.control_frame, textvariable=self.force_var, width=8)force_entry.pack(anchor=tk.W, pady=(0, 5))# 添加冲击时间输入tk.Label(self.control_frame, text="冲击时间 (s):").pack(anchor=tk.W)self.impulse_time_var = tk.StringVar(value="0.1")impulse_time_entry = ttk.Entry(self.control_frame, textvariable=self.impulse_time_var, width=8)impulse_time_entry.pack(anchor=tk.W, pady=(0, 5))# 添加摩擦系数滑动条tk.Label(self.control_frame, text="摩擦系数 μ:").pack(anchor=tk.W)self.mu_frame = tk.Frame(self.control_frame)self.mu_frame.pack(fill=tk.X, pady=(0, 5))self.mu_var = tk.DoubleVar(value=0.1)self.mu_scale = ttk.Scale(self.mu_frame, from_=0.0, to=0.5, orient=tk.HORIZONTAL,variable=self.mu_var, length=120) # 减小滑块长度self.mu_scale.pack(side=tk.LEFT, fill=tk.X, expand=True)self.mu_spinbox = ttk.Spinbox(self.mu_frame, from_=0.0, to=0.5, increment=0.01, textvariable=self.mu_var, width=5) # 减小宽度self.mu_spinbox.pack(side=tk.LEFT, padx=(5, 0))# 摩擦力显示self.friction_var = tk.StringVar(value="摩擦力: 0 N")tk.Label(self.control_frame, textvariable=self.friction_var).pack(anchor=tk.W, pady=(0, 5))# 添加按钮 - 并排显示button_frame = tk.Frame(self.control_frame)button_frame.pack(fill=tk.X, pady=(5, 5))self.push_button = ttk.Button(button_frame, text="推动", command=self.push_object)self.push_button.pack(side=tk.LEFT, padx=(0, 5), fill=tk.X, expand=True)self.reset_button = ttk.Button(button_frame, text="归位", command=self.reset_simulation)self.reset_button.pack(side=tk.LEFT, fill=tk.X, expand=True)# 添加公式说明区域 - 使用Text组件带滚动条self.formula_frame = tk.Frame(self.control_frame, relief=tk.GROOVE, bg="#f9f9f9")self.formula_frame.pack(anchor=tk.W, pady=(5, 0), fill=tk.BOTH, expand=True)# 添加滚动条scrollbar = tk.Scrollbar(self.formula_frame)scrollbar.pack(side=tk.RIGHT, fill=tk.Y)# 使用Text组件self.formula_text = tk.Text(self.formula_frame, wrap=tk.WORD,yscrollcommand=scrollbar.set,bg="#f9f9f9",padx=5,pady=3,height=6, # 初始显示6行width=20, # 固定宽度20字符font=("Courier New", 9), # 等宽字体更紧凑state="disabled" # 设置为只读)self.formula_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)scrollbar.config(command=self.formula_text.yview)# 创建图表self.figure = plt.Figure(figsize=(6, 3))self.ax = self.figure.add_subplot(111)self.ax.set_xlabel('时间 (s)')self.ax.set_ylabel('位置 (m)')self.ax.set_title('物体运动情况')# 设置网格和刻度self.ax.grid(True, linestyle='--', alpha=0.7)self.ax.xaxis.set_major_locator(MultipleLocator(1))self.ax.yaxis.set_major_locator(MultipleLocator(10))self.ax.xaxis.set_minor_locator(MultipleLocator(0.5))self.ax.yaxis.set_minor_locator(MultipleLocator(5))self.ax.grid(which='minor', linestyle=':', alpha=0.4)# 调整图表边距self.figure.subplots_adjust(left=0.10, right=0.95, top=0.95, bottom=0.12) # 减小左边距self.canvas = FigureCanvasTkAgg(self.figure, master=self.figure_frame)self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)# 创建物体动画区域self.canvas_width = 800self.canvas_height = 220self.simulation_canvas = tk.Canvas(self.bottom_frame, width=self.canvas_width, height=self.canvas_height, bg="white")self.simulation_canvas.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)# 初始化物体self.block_x = 10self.block = self.simulation_canvas.create_rectangle(self.block_x, 50, self.block_x + self.block_size, 50 + self.block_size, fill="olive", outline="black")# 绘制刻度尺self.draw_ruler()# 添加木棒对象self.stick = self.simulation_canvas.create_rectangle(0, 0, 0, 0,fill="brown", outline="black", state="hidden")# 更新摩擦力显示self.mu_var.trace_add("write", self.update_friction_display)self.update_friction_display()# 初始化公式说明self.reset_simulation()def draw_ruler(self):# 获取物体右侧位置block_right = self.block_x + self.block_size # 应该等于50# 绘制水平线 - 覆盖整个画布y_pos = 100self.simulation_canvas.create_line(10, y_pos + self.block_size/2, self.canvas_width - 10, y_pos + self.block_size/2, fill="blue", width=1, tags="ruler")# 固定长度的刻度尺 - 70米max_length_meters = 70max_length_pixels = max_length_meters * 10 # 每米10像素# 绘制固定长度的刻度尺 - 从物体右侧位置开始# 每10像素一个小刻度,每50像素一个中等刻度,每100像素一个大刻度for i in range(0, max_length_pixels, 10):x_pos = block_right + iif i % 100 == 0: # 大刻度tick_height = 8self.simulation_canvas.create_line(x_pos, y_pos + self.block_size/2, x_pos, y_pos + self.block_size/2 + tick_height, fill="black", width=1.5, tags="ruler")# 添加数字标签(米)self.simulation_canvas.create_text(x_pos, y_pos + self.block_size/2 + 15, text=str(i // 10), fill="black", font=("Arial", 8), tags="ruler")elif i % 50 == 0: # 中等刻度tick_height = 5self.simulation_canvas.create_line(x_pos, y_pos + self.block_size/2, x_pos, y_pos + self.block_size/2 + tick_height, fill="black", width=1, tags="ruler")else: # 小刻度tick_height = 3self.simulation_canvas.create_line(x_pos, y_pos + self.block_size/2, x_pos, y_pos + self.block_size/2 + tick_height, fill="black", width=0.5, tags="ruler")# 在刻度尺0位置添加红色标记,明确表示这是0点self.simulation_canvas.create_line(block_right, y_pos + self.block_size/2 - 10,block_right, y_pos + self.block_size/2 + 15,fill="red", width=1.5, tags="ruler")self.simulation_canvas.create_text(block_right, y_pos + self.block_size/2 + 25,text="0", fill="red", font=("Arial", 9, "bold"), tags="ruler")def update_friction_display(self, *args):try:mass = float(self.mass_var.get())mu = self.mu_var.get()gravity = 9.8friction = mu * mass * gravityself.friction_var.set(f"摩擦力: {friction:.2f} N")except ValueError:self.friction_var.set("摩擦力: 计算错误")def push_object(self):if self.is_moving:returntry:mass = float(self.mass_var.get())initial_force = float(self.force_var.get())mu = self.mu_var.get()except ValueError:return# 禁用按钮,防止多次点击self.push_button.configure(state="disabled")# 重置标志self.zero_friction_warning_shown = False# 显示推动木棒self.show_push_stick()def show_push_stick(self):# 设置木棒的初始位置(物体左侧)stick_width = 40stick_height = 10block_x = self.simulation_canvas.coords(self.block)[0] # 获取物体当前X坐标# 计算木棒位置 - 初始位置在物体左侧一段距离stick_x = block_x - stick_width - 10stick_y = 50 + self.block_size/2 - stick_height/2 # 与物体中心对齐# 更新木棒位置并显示self.simulation_canvas.coords(self.stick,stick_x, stick_y,stick_x + stick_width, stick_y + stick_height)self.simulation_canvas.itemconfigure(self.stick, state="normal")# 设置木棒推动动画self.animate_stick(stick_x, stick_y, stick_width, stick_height)def animate_stick(self, stick_x, stick_y, stick_width, stick_height, step=0):if step < 10: # 推动动画步骤# 计算每一步木棒移动的距离move_distance = 2# 更新木棒位置self.simulation_canvas.coords(self.stick,stick_x + move_distance * step, stick_y,stick_x + stick_width + move_distance * step, stick_y + stick_height)# 继续下一步动画self.root.after(30, lambda: self.animate_stick(stick_x, stick_y, stick_width, stick_height, step + 1))else:# 推动完成后,隐藏木棒self.simulation_canvas.itemconfigure(self.stick, state="hidden")# 然后开始物体的运动self.start_object_motion()def start_object_motion(self):try:mass = float(self.mass_var.get())initial_force = float(self.force_var.get())impulse_time = float(self.impulse_time_var.get())mu = self.mu_var.get()except ValueError:self.push_button.configure(state="normal") # 如果出错,重新启用按钮return# 重置数据# 正确计算初速度:v = F·t/m (冲量/质量)impulse = initial_force * impulse_time # 冲量 = 力 × 时间self.velocity = impulse / mass # 速度变化 = 冲量/质量self.position = 0 # 这里是物理位置,从0开始self.time_data = [0]self.position_data = [0]self.is_moving = True# 清除之前的线条if self.line:self.line.remove()# 处理摩擦系数为0的特殊情况if mu <= 0.0001: # 近似为0的情况estimated_max_distance = 70 # 限制为70米,与刻度尺一致else:# 计算理论最大位移:s = v²/(2·μ·g)estimated_max_distance = (self.velocity ** 2) / (2 * mu * 9.8)# 限制最大位移不超过70米estimated_max_distance = min(estimated_max_distance, 70)# 设置更合理的y轴刻度范围y_max = max(10, estimated_max_distance * 1.2) # 确保至少显示10米,最大不超过70米的120%y_max = min(y_max, 70) # 确保不超过70米# 重置坐标轴 - 更合理的范围self.ax.set_xlim(0, 10)self.ax.set_ylim(0, y_max)# 更新公式说明标签显示当前计算结果formula_text = "当前计算结果:\n"formula_text += f"• 初速度 = {self.velocity:.2f} m/s\n"formula_text += f" (F={initial_force}N, t={impulse_time}s, m={mass}kg)\n\n"if mu > 0.0001:formula_text += f"• 预计最大位移 = {estimated_max_distance:.2f} m\n"formula_text += f" (v0={self.velocity:.2f}m/s, μ={mu})\n\n"else:formula_text += "• 无摩擦力,理论上会无限运动\n\n"formula_text += "公式说明:\n"formula_text += "• 瞬间推力(冲量): I = F·Δt\n"formula_text += "• 初速度: v0 = F·t/m\n"formula_text += "• 最大位移: s = v0^2/(2·μ·g)"#self.formula_label.config(text=formula_text)self.formula_text.config(state="normal")self.formula_text.delete(1.0, tk.END)self.formula_text.insert(tk.END, formula_text)self.formula_text.config(state="disabled") # 绘制新的线条self.line, = self.ax.plot(self.time_data, self.position_data, 'b-', linewidth=1)# 启动动画self.animate()# 恢复按钮状态self.push_button.configure(state="normal")def animate(self):if not self.is_moving:returntry:mass = float(self.mass_var.get())mu = self.mu_var.get()except ValueError:return# 计算摩擦力gravity = 9.8friction_force = mu * mass * gravity# 计算加速度 (负的,因为摩擦力阻碍运动)acceleration = -friction_force / mass# 更新时间、速度和位置 (时间步长0.05秒)dt = 0.05new_time = self.time_data[-1] + dt# 摩擦系数为0或近似为0时特殊处理if mu <= 0.0001:new_velocity = self.velocity # 速度不变# 检查物体是否将要超出画布new_position = self.position_data[-1] + new_velocity * dtcanvas_limit = 70 # 限制为70米if new_position >= canvas_limit and not self.zero_friction_warning_shown:# 停止动画self.is_moving = False# 显示永恒运动的警告对话框self.zero_friction_warning_shown = Truemessagebox.showinfo("永恒运动", "由于没有摩擦力,物体将永远运动下去!在现实世界中,物体最终会因为空气阻力等因素停下来。")returnelse:# 更新速度 (不能低于0)new_velocity = max(0, self.velocity + acceleration * dt)# 如果速度变为0或接近0,停止运动if new_velocity < 0.01: # 使用小阈值判断速度接近0self.is_moving = False# 确保最后一个位置是静止位置new_position = self.position_data[-1]# 更新数据以显示最终状态self.time_data.append(new_time)self.position_data.append(new_position)self.velocity = 0# 更新图表最终状态self.line.set_data(self.time_data, self.position_data)self.canvas.draw_idle()return# 更新位置 - 使用更精确的运动学公式new_position = self.position_data[-1] + self.velocity * dt + 0.5 * acceleration * dt * dt# 更新数据self.time_data.append(new_time)self.position_data.append(new_position)self.velocity = new_velocity# 更新图表self.line.set_data(self.time_data, self.position_data)# 更智能地调整x轴范围if new_time > self.ax.get_xlim()[1]:self.ax.set_xlim(0, new_time * 1.2)# 限制y轴最大值为70m或当前位置的120%,确保与刻度尺一致current_y_max = self.ax.get_ylim()[1]if new_position > current_y_max * 0.8: # 如果位置超过当前最大值的80%new_y_max = min(70, new_position * 1.2) # 限制最大值为70米self.ax.set_ylim(0, new_y_max)self.canvas.draw_idle()# 获取物体右侧位置(初始值)block_right = self.block_x + self.block_size# 计算画布上的位置 - 物体右侧对应物理位置0点scaled_position = block_right + new_position * 10 # 缩放以适应画布# 检查是否超出画布边界if scaled_position > self.canvas_width - 10:# 如果物体即将超出画布self.is_moving = False# 根据摩擦系数提供不同的提示信息if mu <= 0.0001:messagebox.showinfo("永恒运动", "由于没有摩擦力,物体将永远运动下去!在现实世界中,物体最终会因为空气阻力等因素停下来。")else:messagebox.showinfo("提示", "物体已到达画布边界")return# 更新物体位置 - 确保物体右侧对应目前位置self.simulation_canvas.coords(self.block, scaled_position - self.block_size, 50, # 左侧坐标scaled_position, 50 + self.block_size # 右侧坐标)# 继续动画if self.is_moving:self.animation_id = self.root.after(50, self.animate)def reset_simulation(self):# 停止动画if self.animation_id:self.root.after_cancel(self.animation_id)self.animation_id = Noneself.is_moving = Falseself.zero_friction_warning_shown = False # 重置警告标志# 重置位置数据self.velocity = 0self.position = 0# 确保木棒隐藏self.simulation_canvas.itemconfigure(self.stick, state="hidden")# 确保按钮可用self.push_button.configure(state="normal")# 重置图表if self.line:self.line.remove()self.line = Noneself.time_data = []self.position_data = []self.ax.set_xlim(0, 10)self.ax.set_ylim(0, 10)self.canvas.draw_idle()# 重置物体位置self.simulation_canvas.coords(self.block, self.block_x, 50, self.block_x + self.block_size, 50 + self.block_size)# 清除并重绘刻度尺self.simulation_canvas.delete("ruler") # 删除所有带有"ruler"标签的项目self.draw_ruler()# 更新公式说明标签formula_text = "公式说明:\n"formula_text += "• 初速度: v0 = F·t/m\n"formula_text += " (F=推力, t=冲击时间, m=质量)\n\n"formula_text += "• 最大位移: s = v0^2/(2·μ·g)\n"formula_text += " (v0=初速度, μ=摩擦系数, g=9.8m/s²)\n\n"formula_text += "• 无摩擦时(μ=0): 物体保持匀速运动"#self.formula_label.config(text=formula_text)self.formula_text.config(state="normal")self.formula_text.delete(1.0, tk.END)self.formula_text.insert(tk.END, formula_text)self.formula_text.config(state="disabled") if __name__ == "__main__":root = tk.Tk()app = MotionSimulator(root)root.mainloop()