用Manim实现高尔顿板(Galton Board)

高尔顿板的介绍

 高尔顿板(Galton Board),有时也称为贝尔图(Bean Machine),是由英国统计学家弗朗西斯·高尔顿(Francis Galton)于19世纪末发明的一种物理装置,用于演示随机分布和大数法则的概念。它通过简单的机械原理展示了概率和统计的基本概念。

高尔顿板是一个简单而有效的工具,通过直观的物理演示使得复杂的概率和统计概念变得易于理解。它不仅是教育的有效工具,也是研究随机性和分布特性的重要模型。

结构与原理

  1. 结构:

    • 高尔顿板通常由一个倾斜的木板或其他材料制成,面板上排列着若干个固定的小钉或障碍物,形成一个网格状的结构。底部有多个接收容器(例如小盒子或小槽),用于收集掉落的颗粒或小球。
  2. 工作原理:

    • 顶部的槽(或投入口)用于放置小球。当小球从顶部落下时,它们会碰到网格中的钉子。每次碰撞时,小球都有50%的概率向左或向右偏移,导致小球沿着随机路径向下移动。
    • 随着小球不断下落,它们最终将停在底部的接收容器中。由于每个球的下落路径是随机的,经过多次实验后,落入各个槽中的小球数量会呈现出明显的钟形正态分布。

数学与统计意义

  • 大数法则: 高尔顿板是展示大数法则的经典案例之一。随着投入的小球数量的增加,落入各个接收容器的数量趋向于正态分布,即使小球的每次下落是随机的,但总体的结果表现出稳定的模式。
  • 中立性和随机性: 高尔顿板展示了随机性下的平衡现象。虽然每个小球的移动路径是随机的,但它们最终的数量分布却可以预测。

应用

  • 高尔顿板常用于教育和教学,帮助学生理解概率、统计、正态分布、大数法则等概念。
  • 也被广泛应用于统计学、心理学和经济学等其他学科的可视化实验中。

创建manim代码 

from manim import *  
import random  

class GaltonBoard(Scene):  

    # 配置信息  
    config = {  
        "runTime": 16,  # 动画运行时间  
        "itemsTotal": 100,  # 总点数  
        "itemDelayFrames": 1,  # 点出现间隔(帧数)  
        "hexSize": .2,  # 六边形的大小  
        "hexVerticalShift": .6,  # 六边形的垂直偏移  
        "hexGorizontalShift": .4,  # 六边形的水平偏移  
        "hexRowsCount": 7,  # 六边形的行数  
        "firstHexCenterX": -3,  # 第一个六边形的中心x坐标  
        "firstHexCenterY": 3,  # 第一个六边形的中心y坐标  
        "durationSeconds": 2,  # 每个点的运动持续时间  
        "circleRadius": .05,  # 小圆点的半径  
        "firstDot": [-3, 4.3, 0]  # 第一个点的位置  
    }  
    
    frameNumber = 0  # 帧计数器  

    def construct(self):  
        # 创建表格、计数器、六边形、顶点和小点  
        table = self.createTable()  # 生成表格  
        counter = self.createCounter()  # 生成计数器  
        hexagons = self.createHexagons()  # 生成六边形  
        vertices = self.createVertices()  # 生成六边形的顶点  
        items = self.createItems(vertices)  # 生成小点  

        # 帧更新函数  
        def updateFrameFunction(table):  
            durationSeconds = GaltonBoard.config["durationSeconds"]  
            durationFrames = durationSeconds * self.camera.frame_rate  # 单位时间内的帧数  
            self.frameNumber += 1  

            for item in items:  
                if item.isActive and self.frameNumber > item.startFrame:  
                    alpha = (self.frameNumber - item.startFrame) / durationFrames  
                    if (alpha <= 1.0):  
                        point = item.path.point_from_proportion(rate_functions.linear(alpha))  # 获取小点在路径上的位置  
                        item.circle.move_to(point)  # 移动小点  
                    else:  
                        updateCounter()  # 更新计数器  
                        updateStackValue(item.stackIndex)  # 更新堆叠值  
                        item.isActive = False  # 设置点为非活动状态  

        # 更新计数器函数  
        def updateCounter():  
            val = counter[0].get_value()  # 获取计数器当前值  
            val += 1  # 增加计数  
            counter[0].set_value(val)  # 更新计数器的值  

        # 更新堆叠值的函数  
        def updateStackValue(stackValueIndex):  
            cell = table.get_entries((1, stackValueIndex + 1))  # 获取表格中对应单元格  
            val = cell.get_value()  # 获取该单元格的当前值  
            val += 1  # 增加堆叠值  
            cell.set_value(val)  # 更新单元格值  

        # 渲染六边形和表格与计数器  
        self.play(FadeIn(hexagons, run_time=1))  
        self.play(FadeIn(table, run_time=1))  
        self.play(FadeIn(counter, run_time=1))  

        # 为更新函数准备需要更新的对象  
        wrapper = VGroup(table, counter)  
        for item in items:  
            wrapper.add(item.circle)  

        runTime = GaltonBoard.config["runTime"]  
        # 开始更新动画  
        self.play(UpdateFromFunc(wrapper, updateFrameFunction), run_time=runTime)  

        self.wait(3)  # 等待3秒以查看结果  

    def createTable(self):  
        # 创建一个整数表来显示点的堆叠数量  
        table = IntegerTable(  
            [[0, 0, 0, 0, 0, 0, 0, 0],],  # 初始化表格  
            line_config={"stroke_width": 1, "color": Y
                        line_config={"stroke_width": 1, "color": YELLOW},  # 表格线的样式设置  
            cell_config={"stroke_width": 1, "color": WHITE},  # 单元格的样式设置  
        )  
        table.move_to(UP * 3)  # 将表格移动到画面上方  
        return table  

    def createCounter(self):  
        # 创建一个计数器用于计数通过的点  
        counter = DecimalNumber(0)  # 创建一个数值对象,初始值为0  
        counter.move_to(UP * 3 + RIGHT * 5)  # 将计数器移动到适当位置  
        return [counter]  # 返回计数器对象列表  

    def createHexagons(self):  
        hexagons = VGroup()  # 创建一个用于存放六边形的组  
        hexSize = GaltonBoard.config["hexSize"]  # 获取六边形的大小  
        hexVerticalShift = GaltonBoard.config["hexVerticalShift"]  # 获取垂直偏移量  
        hexGorizontalShift = GaltonBoard.config["hexGorizontalShift"]  # 获取水平偏移量  
        hexRowsCount = GaltonBoard.config["hexRowsCount"]  # 获取行数  

        # 循环生成六边形  
        for row in range(hexRowsCount):  
            for col in range(3):  
                hexagon = RegularPolygon(n=6, radius=hexSize)  # 创建一个六边形  
                hexagon.move_to(  
                    (col * hexGorizontalShift, row * hexVerticalShift, 0)  # 设置六边形位置  
                )  
                hexagons.add(hexagon)  # 将六边形加入组中  

        return hexagons  # 返回所有六边形  

    def createVertices(self):  
        # 创建六边形的顶点坐标  
        vertices = []  
        hexSize = GaltonBoard.config["hexSize"]  # 获取六边形的大小  
        hexVerticalShift = GaltonBoard.config["hexVerticalShift"]  # 获取垂直偏移量  

        # 根据行数计算每行的顶点坐标  
        for row in range(GaltonBoard.config["hexRowsCount"]):  
            vertexRow = []  
            for i in range(3):  # 每行有3个顶点  
                vertexRow.append(np.array([  
                    i * GaltonBoard.config["hexGorizontalShift"],  
                    row * hexVerticalShift,  
                    0  
                ]))  
            vertices.append(vertexRow)  # 将顶点按行添加到列表中  

        return vertices  # 返回所有顶点  

    def createItems(self, vertices):  
        # 创建小点并为其分配路径  
        itemsTotal = GaltonBoard.config["itemsTotal"]  # 获取总点数  
        circleRadius = GaltonBoard.config["circleRadius"]  # 小圆点半径  
        itemDelayFrames = GaltonBoard.config["itemDelayFrames"]  # 小点出现间隔  
        firstDot = GaltonBoard.config["firstDot"]  # 第一个小点的位置  

        items = []  # 存放小点的列表  
        startFrame = 0  # 起始帧计数  
        stackValues = [0] * 9  # 存储堆叠数的列表,初始化为0  

        for k in range(itemsTotal):  
            item = Item()  # 初始化点  
            circle = Circle(radius=circleRadius, color=GREEN, fill_opacity=1)  # 创建小圆点  
            pathIndex = self.createPathIndex()  # 生成路径索引  
            stackIndex = pathIndex.bit_count()  # 计算堆叠索引  
            stackValues[stackIndex] += 1  # 增加堆叠值  

            path = self.createPath(vertices, pathIndex, stackValues[stackIndex])  # 创建路径  

            item.path = path  # 分配路径  
            item.circle = circle  # 分配圆点  
            item.stackIndex = stackIndex  # 设置堆叠索引  
            item.startFrame = startFrame  # 设置起始帧  
            
            startFrame += itemDelayFrames  # 更新起始帧  

            self.add(circle)  # 将圆点添加到场景中  
            circle.move_to(firstDot)  # 移动圆点到第一个位置  

            items.append(item)  # 将点添加到列表中  

            # 如果需要可以显示路径  
            # self.add(path)  

        return items  # 返回所有小点  

    def createPathIndex(self):  
        # 随机生成一个路径索引  
        return random.randrange(128)  # 返回0到127之间的随机整数  

    def createPath(self, vertices, pathIndex, itemsCountInStack):  
        # 根据路径索引和堆叠数创建路径  
        firstDot = GaltonBoard.config["firstDot"]  # 获取第一个点的位置  
        rowCapacity = 3  # 每行最大容量  

        # 计算最后一个点在网格中的位置  
        lastDotRowIndex = (itemsCountInStack - 1) // rowCapacity  
        lastDotColIndex = (itemsCountInStack - 1) % rowCapacity   
        
        path = Line(firstDot, vertices[0][0], stroke_width=1)  # 创建起始点到第一个点的线  
        previousDot = vertices[0][0]  
        binary = bin(pathIndex)[2:].zfill(7)  # 将路径索引转为二进制,左侧填0到7位  
        rowIndex, colIndex = 1, 0  # 初始化行列索引  

        # 根据路径索引的二进制值生成路径  
        for digit in binary:  
            if digit == '0':  
                pathTmp = ArcBetweenPoints(previousDot, vertices[rowIndex][colIndex], angle=PI / 2, stroke_width=1)  # 向左转90度  
            else:  
                colIndex += 1  
                pathTmp = ArcBetweenPoints(previousDot, vertices[rowIndex][colIndex], angle=-PI / 2, stroke_width=1)  # 向右转90度  
            previousDot = vertices[rowIndex][colIndex]  
            path.append_vectorized_mobject(pathTmp)  # 将路径片段添加到路径中  
            rowIndex += 1  

        # 计算最后一个点的坐标  
        lastDotWidth = .1  # 最后一个点的宽度  
        lastDotHeight = .1  # 最后一个点的高度  
        lastDotX = previousDot[0]  # 获取最后一个点的x坐标  
        
        # 根据最后点的位置调整x坐标  
        if lastDotColIndex == 0:  
            lastDotX -= lastDotWidth  
        elif lastDotColIndex == 2:  
            lastDotX += lastDotWidth  

        lastDotY = previousDot[1] - 2.4 + lastDotHeight * lastDotRowIndex  # 计算最后一个点的y坐标  

        pathLast = Line(previousDot, [lastDotX, lastDotY, 0], stroke_width=1)  # 连接到最后一个点的路径  
        path.append_vectorized_mobject(pathLast)  # 将最后的路径段添加到路径中  

        return path  # 返回生成的路径  

    def showDotMap(self, showAxes):  
        # 显示点的坐标图  
        for x in range(-7, 8):  
            for y in range(-4, 5):  
                dot = Dot(np.array([x, y, 0]), radius=0.02)  # 创建一个小点  
                self.add(dot)  # 将点添加到场景中  

        if showAxes:  
            ax = Axes(x_range=[-7, 7], y_range=[-4, 4], x_length=14, y_length=8)  # 创建坐标轴  
            self.add(ax)  # 将坐标轴添加到场景中  

class Item:  
    # 定义小点的类  
    circle = None  # 圆点  
    path = None  # 路径  
    startFrame = 0  # 开始帧  
    stackIndex = 0  # 堆叠索引  
    isActive = True  # 是否活动的标志

 我想要的理想型结果:

实际运行结果:

代码解释

  1. GaltonBoard 类: 该类继承自 Manim 的 Scene,用于创建高尔顿板的动画。配置参数定义了高尔顿板的运行时间、点的总数、点之间的延迟、圆点的大小和位置等信息。

  2. 构造函数construct 方法是动画的主入口,创建所有组件(表格、计数器、六边形、顶点、小点等),并控制它们的动画效果。

  3. 创建六边形和顶点createHexagons 和 createVertices 方法用于生成高尔顿板上的六边形及其顶点,以便点沿着这些顶点掉落。

  4. 生成路径和小点createItems 方法创建小点并为其分配路径,路径的生成基于随机索引,决定了每个点在高尔顿板上掉落的方向。

  5. 动画更新: 动画通过 UpdateFromFunc 不断更新每个小点的位置,直到所有小点都掉落完毕。

  6. 路径生成createPath 方法根据随机生成的索引创建路径,通过计算每个点的坐标来绘制连线。

  7. 计数器和堆叠统计: 使用计数器记录每个点通过的次数,并在界面上显示。

总结

此代码实现了一种经典的概率分布演示工具,通过高尔顿板的随机掉落过程展示大数法则,提供了视觉化的理解,并使用 Manim 库进行高效的动画展示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yasen.M

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值