python中学物理实验模拟:瞬间推力与摩擦力作用下的物体运动

发布于:2025-06-29 ⋅ 阅读:(14) ⋅ 点赞:(0)

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 MultipleLocator

class MotionSimulator:
    def __init__(self, root):
        self.root = root
        self.root.title("瞬间推力与摩擦力作用下的物体运动")
        self.root.geometry("900x780")  # 减小窗口宽度
        
        # 设置中文支持
        plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
        plt.rcParams['axes.unicode_minus'] = False
        
        # 初始化物体和动画参数
        self.block_size = 40
        self.velocity = 0
        self.position = 0
        self.animation = None
        self.is_moving = False
        self.time_data = []
        self.position_data = []
        self.line = None
        self.animation_id = None
        self.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 = 800
        self.canvas_height = 220
        self.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 = 10
        self.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 = 100
        self.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 = 70
        max_length_pixels = max_length_meters * 10  # 每米10像素
        
        # 绘制固定长度的刻度尺 - 从物体右侧位置开始
        # 每10像素一个小刻度,每50像素一个中等刻度,每100像素一个大刻度
        for i in range(0, max_length_pixels, 10):
            x_pos = block_right + i
            
            if i % 100 == 0:  # 大刻度
                tick_height = 8
                self.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 = 5
                self.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 = 3
                self.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.8
            friction = mu * mass * gravity
            self.friction_var.set(f"摩擦力: {friction:.2f} N")
        except ValueError:
            self.friction_var.set("摩擦力: 计算错误")
    
    def push_object(self):
        if self.is_moving:
            return
        
        try:
            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 = 40
        stick_height = 10
        block_x = self.simulation_canvas.coords(self.block)[0]  # 获取物体当前X坐标
        
        # 计算木棒位置 - 初始位置在物体左侧一段距离
        stick_x = block_x - stick_width - 10
        stick_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:
            return
        
        try:
            mass = float(self.mass_var.get())
            mu = self.mu_var.get()
        except ValueError:
            return
        
        # 计算摩擦力
        gravity = 9.8
        friction_force = mu * mass * gravity
        
        # 计算加速度 (负的,因为摩擦力阻碍运动)
        acceleration = -friction_force / mass
        
        # 更新时间、速度和位置 (时间步长0.05秒)
        dt = 0.05
        new_time = self.time_data[-1] + dt
        
        # 摩擦系数为0或近似为0时特殊处理
        if mu <= 0.0001:
            new_velocity = self.velocity  # 速度不变
            
            # 检查物体是否将要超出画布
            new_position = self.position_data[-1] + new_velocity * dt
            canvas_limit = 70  # 限制为70米
            
            if new_position >= canvas_limit and not self.zero_friction_warning_shown:
                # 停止动画
                self.is_moving = False
                
                # 显示永恒运动的警告对话框
                self.zero_friction_warning_shown = True
                messagebox.showinfo("永恒运动", "由于没有摩擦力,物体将永远运动下去!在现实世界中,物体最终会因为空气阻力等因素停下来。")
                return
        else:
            # 更新速度 (不能低于0)
            new_velocity = max(0, self.velocity + acceleration * dt)
            
            # 如果速度变为0或接近0,停止运动
            if new_velocity < 0.01:  # 使用小阈值判断速度接近0
                self.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 = None
        
        self.is_moving = False
        self.zero_friction_warning_shown = False  # 重置警告标志
        
        # 重置位置数据
        self.velocity = 0
        self.position = 0
        
        # 确保木棒隐藏
        self.simulation_canvas.itemconfigure(self.stick, state="hidden")
        
        # 确保按钮可用
        self.push_button.configure(state="normal")
        
        # 重置图表
        if self.line:
            self.line.remove()
            self.line = None
        self.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()