ZhangZhihui's Blog  

 

class SinAndCosFunctionPlot(Scene):
    def construct(self):
        axes = Axes(
            x_range=[-10, 10.3, 1],
            y_range=[-1.5, 1.5, 1],
            x_length=10,
            axis_config={'color': GREEN},
            x_axis_config={
                'numbers_to_include': np.arange(-10, 10.01, 2),
                'numbers_with_elongated_ticks': np.arange(-10, 10.01, 2),
            },
            tips=False,
        )
        axes_labels = axes.get_axis_labels()
        sin_graph = axes.plot(lambda x: np.sin(x), color=BLUE)
        cos_graph = axes.plot(lambda x: np.cos(x), color=RED)

        sin_label = axes.get_graph_label(sin_graph, '\\sin(x)', x_val=-10, direction=UP / 2)
        cos_label = axes.get_graph_label(cos_graph, label='\\cos(x)')

        vert_line = axes.get_vertical_line(axes.i2gp(TAU, cos_graph), color=YELLOW, line_func=Line)
        line_label = axes.get_graph_label(cos_graph, r'x=2\pi', x_val=TAU, direction=UR, color=WHITE)

        plot = VGroup(axes, sin_graph, cos_graph, vert_line)
        labels = VGroup(axes_labels, sin_label, cos_label, line_label)
        self.add(plot, labels)

 

x_length=10是什么意思?

 

在 Manim 的 Axes 类中,x_length 是一个非常关键的参数,它决定了 x 轴在屏幕上的物理长度(视觉长度)

为了让你彻底理解,我们需要区分两个概念:坐标数值范围 vs 屏幕物理尺寸


1. x_range vs x_length

  • x_range=[-10, 10.3, 1]:这是 数学意义上的定义。它告诉 Manim 你的 x 轴是从数字 -10 数到 10.3,每隔 1 个单位画一个刻度。

  • x_length=10:这是 视觉意义上的定义。它告诉 Manim 这个 x 轴在显示器上总共要占 10 个 Manim 单位 的宽度。

注: 在 Manim 的默认视角中,屏幕的总宽度通常是 14.22 个单位(16:9 比例)。所以 x_length=10 意味着这条轴会占据屏幕横向约 70% 的位置。


2. 为什么要设置 x_length

如果你不设置 x_length,Manim 会根据默认比例来绘制,这可能会导致两个问题:

  1. 比例失调:如果你画一个从 0 到 1000 的坐标轴,不限制物理长度的话,它可能会画得超级长,直接超出屏幕。

  2. 单位换算x_length 决定了 “1 个数学单位 = 多少个屏幕单位”

    • 在你的代码中:数学跨度约为 $10.3 - (-10) = 20.3$。

    • 物理长度是 $10$。

    • 这意味着坐标轴上每增加 $1$ 个单位,在屏幕上实际只移动了约 <span class="math-inline" data-math="0.49" data-index-in-node="30">$0.49$ (<span class="math-inline" data-math="10 / 20.3" data-index-in-node="36">$10 / 20.3$) 个单位。


3. 它的实用性

通过调整 x_lengthy_length,你可以控制图表的长宽比

  • 如果你想要一个很宽、很扁的图表,就把 x_length 调大,y_length 调小。

  • 如果你想要一个正方形的坐标系,且坐标范围不对称,你就必须通过手动设置这两个参数来强行对齐视觉效果。


4. 代码中的影响

在你的代码里,由于设置了 x_length=10

Python
 
axes = Axes(
    x_range=[-10, 10.3, 1], # 数学范围
    x_length=10,            # 物理宽度
    ...
)

当你之后调用 axes.plot(lambda x: np.sin(x)) 时,Manim 会自动根据这个比例,把 $\sin(x)$ 的曲线压缩在那个 10 单位宽的物理范围内。

总结 (TL;DR)

x_length 就是 这条轴在屏幕上看起来有多长(用 Manim 的坐标单位衡量)。它不改变坐标轴上的数字,只改变这些数字在屏幕上被“拉伸”或“压缩”的程度。

 

 

"numbers_to_include"和"numbers_with_elongated_ticks"是什么意思?

 

这两个参数是 x_axis_config(坐标轴配置词典)中的高级选项,专门用来精细化控制坐标轴上的数字标注刻度线样式


1. numbers_to_include (要显示的数字)

默认情况下,Manim 的坐标轴可能不会在每个刻度都写上数字(为了防止拥挤)。

  • 功能:明确指定哪些坐标点下方需要显示数值。

  • 在你的代码中np.arange(-10, 10.01, 2) 生成的是 [-10, -8, -6, ..., 8, 10]

  • 效果:坐标轴上每隔 2 个单位就会标出一个数字。即使你的 x_range 步长设置得很小(比如 0.5),数字也只会出现在你指定的这几个点上。


2. numbers_with_elongated_ticks (长刻度线)

在一个专业的图表中,我们通常会区分主刻度(Major Ticks)和次刻度(Minor Ticks)。

  • 功能:指定哪些位置的刻度线(那根小竖线)需要加长

  • 在你的代码中:你传入了和数字标注相同的数组。

  • 效果:所有标了数字的地方,其刻度线会比其他地方更长、更明显。这在视觉上起到了“强调”作用,让观众一眼看出坐标系的主次关系。


3. 视觉效果解析

如果你把这两个参数结合起来用,你会得到如下效果:

坐标位置 是否有数字 刻度线长度
-10, -8, -6... 有数字 长刻度 (Elongated)
-9, -7, -5... 无数字 短刻度 (Standard)

4. 为什么要用 np.arange

注意到你代码中写的是 10.01 而不是 10

Python
 
np.arange(-10, 10.01, 2)

这是 Python 处理浮点数的一个小技巧。因为 np.arange 是左闭右开区间,如果写 10,由于精度误差,最后那个 10 可能被漏掉。写成 10.01 能确保生成的数组稳稳地包含 10 这个端点。

5. 进阶提示:如何修改长刻度的长度?

如果你觉得默认的长刻度还不够长,你可以在 axis_configx_axis_config 中添加 long_tick_length

Python
 
x_axis_config={
    "numbers_to_include": np.arange(-10, 11, 2),
    "numbers_with_elongated_ticks": np.arange(-10, 11, 2),
    "long_tick_length": 0.4, # 默认通常更短
    "tick_length": 0.1,      # 普通刻度的长度
}

总结:

  • numbers_to_include 决定“哪儿写字”。

  • numbers_with_elongated_ticks 决定“哪儿的刻度线更长”。

    两者配合使用,能让你的坐标轴看起来非常规范,就像数学课本上的标准插图一样。

 

 

tips=False是什么意思?

 

在 Manim 的 Axes(坐标轴)或 NumberLine(数轴)中,tips=False 的意思是:不在坐标轴的末端绘制箭头。

默认情况下,Manim 的坐标轴末端会有一个三角形的箭头(Tip),用来指示数值增加的正方向。

1. 视觉对比

  • tips=True (默认值):坐标轴的 $x$ 轴右端和 <span class="math-inline" data-math="y" data-index-in-node="28">$y$ 轴上端会有小箭头。这符合大多数数学教科书展示函数图像的习惯。

  • tips=False:坐标轴就是简单的直线。这种风格在现代数据可视化、商业报表或者某些极简风格的动画中非常常见。


2. 为什么在你的代码中要设置 False

在处理像 正弦/余弦函数 这种周期性、无限延伸的函数时,有时候我们希望坐标轴看起来像是一个“窗口”,而不是一个带有方向指示的射线:

  1. 美观考量:当你的 $x$ 轴范围是对称的(比如 -10 到 10),左右两端都没有箭头会让画面看起来更平衡。

  2. 避免遮挡:在某些情况下,箭头可能会遮挡住坐标轴末端附近的标签或数据点。

  3. 风格统一:如果你的绘图区域(Plot)是一个封闭的矩形区域,去掉箭头会让整体感更强。


3. 如果想局部控制怎么办?

如果你希望 $x$ 轴有箭头,但 <span class="math-inline" data-math="y" data-index-in-node="15">$y$ 轴没有,你可以针对性地写在各自的配置里:

Python
 
axes = Axes(
    x_axis_config={"tips": True},
    y_axis_config={"tips": False},
)

4. 延伸:如何改变箭头的大小?

如果你觉得默认箭头太丑或者太大,你可以通过 tip_config 来精细调整,而不是直接关掉它:

Python
 
axes = Axes(
    tips=True,
    axis_config={
        "tip_config": {
            "tip_shape": StealthTip, # 换一种更尖、更现代的箭头形状
            "width": 0.2,            # 箭头的宽度
            "length": 0.2,           # 箭头的长度
        }
    }
)

总结:

tips=False 就是一个“开关”,负责隐藏坐标轴末端的箭头,让线条保持平直。

 

 

axes.i2gp(TAU, cos_graph) 是什么意思?

 

在 Manim 的 Axes(坐标轴)对象中,i2gp 是一个非常高效的缩写方法。它的全称是 input_to_graph_point

简单来说,它的作用是:根据给定的 $x$ 值,自动计算出它在某个函数图像(Graph)上对应的坐标点(Point)。


1. 拆解分析

在你这段代码中:

axes.i2gp(TAU, cos_graph)

  • i (input):输入的 $x$ 轴数值。这里是 <code data-path-to-node="5,0,0" data-index-in-node="24">TAU</code>(即 <span class="math-inline" data-math="2\pi \approx 6.28" data-index-in-node="30">$2\pi \approx 6.28$)。

  • 2 (to):转换到。

  • gp (graph_point):图像上的点。

  • cos_graph:参考的函数曲线。

它的逻辑是: “在 axes 坐标系下,找到当 $x = 2\pi$ 时,曲线 <code data-path-to-node="6" data-index-in-node="38">cos_graph</code> 所处的那个位置的 <span class="math-inline" data-math="[x, y, z]" data-index-in-node="57">$[x, y, z]$ 坐标。&rdquo;


2. 为什么要用它?(对比直接算坐标)

如果你不使用 i2gp,你可能得手动计算坐标:

Python
 
# 这太麻烦了,而且容易算错
point = axes.c2p(TAU, np.cos(TAU)) 
  • c2p (coords_to_point) 只需要坐标数值。

  • i2gp 更聪明,它直接绑定了曲线对象。如果你以后把 cos_graph 换成了 sin_graph,或者改变了函数的定义,i2gp 依然能准确地抓到曲线上的那个点,而不需要你重新写一遍数学公式。


3. 在你代码里的实际作用

你代码接下来的部分是:

Python
 
vert_line = axes.get_vertical_line(axes.i2gp(TAU, cos_graph), ...)

这行代码的完整意图是:

  1. 找到 $\cos(x)$ 曲线在 <span class="math-inline" data-math="x = 2\pi" data-index-in-node="15">$x = 2\pi$ 时的那个<strong data-path-to-node="17,0,0" data-index-in-node="28">点</strong>。

  2. 从那个点出发,向 $x$ 轴画一根<strong data-path-to-node="17,1,0" data-index-in-node="15">垂直虚线</strong>。


4. 相关的兄弟方法

为了方便记忆,你可以把这几个常用缩写放在一起看:

缩写 全称 作用
c2p coords_to_point 把数学坐标 $(x, y)$ 转换为屏幕坐标 <span class="math-inline" data-math="[x, y, z]" data-index-in-node="21">$[x, y, z]$。
p2c point_to_coords 把屏幕上的点反推回坐标轴上的数值。
i2gp input_to_graph_point 最常用:给定 $x$,找曲线上的点。

总结

axes.i2gp(TAU, cos_graph) 就是在问 Manim:“帮我看看 $2\pi$ 这个位置,在余弦曲线上对应的那个点的屏幕坐标在哪里?&rdquo; 它常用于绘制切线、法线、垂直线或在曲线上放置动态的小球。

 

 

 

class ArgMinExample(Scene):
    def construct(self):
        ax = Axes(
            x_range=[0, 10],
            y_range=[0, 100, 10],
            axis_config={'include_tip': False}
        )
        labels = ax.get_axis_labels(x_label='x', y_label='f(x)')

        t = ValueTracker(0)

        def func(x):
            return 2 * (x - 5) ** 2
        graph = ax.plot(func, color=MAROON)

        initial_point = [ax.coords_to_point(t.get_value(), func(t.get_value()))]
        dot = Dot(point=initial_point)

        dot.add_updater(lambda x: x.move_to(ax.c2p(t.get_value(), func(t.get_value()))))
        x_space = np.linspace(*ax.x_range[:2], 200)
        minimum_index = func(x_space).argmin()

        self.add(ax, labels, graph, dot)
        self.play(t.animate.set_value(x_space[minimum_index]))
        self.wait()

 

ax.x_range[:2]是什么意思?

 

在 Python 和 Manim 的上下文中,ax.x_range[:2] 是一个典型的切片(Slicing)操作,用来提取坐标轴范围的前两个关键数值。

要理解它,我们需要分两步拆解:

1. ax.x_range 里面装的是什么?

当你定义 Axes 时:

Python
 
x_range=[0, 10] 
# 或者更完整的写法:
x_range=[0, 10, 1] 

Manim 会把这个列表存储在 ax.x_range 属性中。它通常包含三个元素:

  1. start: 最小值(这里是 0

  2. end: 最大值(这里是 10

  3. step: 步长/刻度间距(这里默认为 1

所以,此时 ax.x_range 的值实际上是 [0, 10, 1]


2. [:2] 切片做了什么?

在 Python 列表操作中,[:2] 表示“从索引 0 开始提取,直到索引 2(但不包括 2)”。

  • ax.x_range[0]0

  • ax.x_range[1]10

  • ax.x_range[2]1

执行 ax.x_range[:2] 后,你得到的是一个新的列表:[0, 10]。它只保留了起始值和结束值,丢弃了步长。


3. 为什么代码里要加上 * 号?

注意你的完整代码行:

Python
 
x_space = np.linspace(*ax.x_range[:2], 200)

这里的 *参数解包(Unpacking) 运算符。它相当于把列表里的两个数拆开,作为独立的参数传给 np.linspace

这行代码等同于:

Python
 
x_space = np.linspace(0, 10, 200)

它的意图非常明确:010 之间均匀地生成 200 个点。这通常是为了遍历整个 x 轴的范围,从而计算出函数在哪些位置有最小值(即后续的 argmin() 操作)。

4. 为什么要这样写,而不是直接写 np.linspace(0, 10, 200)

这体现了 “代码复用”“参数联动” 的好习惯:

  • 同步更新:如果你以后把 Axesx_range 改成了 [-5, 5],下面计算 x_space 的代码完全不需要修改。它会自动提取新的范围进行计算。

  • 减少 Bug:手动输入两次数字很容易出现“改了上面忘了下面”的情况,导致图像范围和计算范围不匹配。


总结

  • ax.x_range: 坐标轴的范围数组(如 [左边界, 右边界, 步长])。

  • [:2]: 只取 [左边界, 右边界]

  • *: 把这两个边界值拆散,塞进 np.linspace 函数里。

这种写法确保了你的数学计算范围始终与你画出来的坐标轴物理范围保持绝对一致

 

 

minimum_index = func(x_space).argmin()

func()是一个自定义的函数,也有argmin()方法吗?

 

这正是 Python 强大(但也容易让人困惑)的地方。func 本身确实没有 argmin() 方法,但它的返回值有。

这里的关键在于 func(x_space) 这一步发生了什么。

1. 发生了什么?

在你的代码中:

  1. x_space 是通过 np.linspace 生成的一个 NumPy 数组(包含 200 个数字)。

  2. func(x_space):当你把一个 NumPy 数组传进你的自定义函数 2 * (x - 5) 2 时,NumPy 会进行 广播(Broadcasting)。它会对数组里的每一个数字分别进行平方和乘法运算。

  3. 结果func(x_space) 执行完后,返回的不再是一个单一的数字,而是一个包含了 200 个函数计算结果的 NumPy 数组

2. 谁拥有 argmin()

既然 func(x_space) 返回的是一个 NumPy 数组对象,那么这个返回的对象自然就拥有了 NumPy 数组的所有方法,其中就包括 .argmin()

所以,这行代码的链式逻辑是:

func(x_space) (得到结果数组) $\rightarrow$ 调用该数组的 .argmin()

3. argmin() 到底在干什么?

argmin() 的作用不是返回最小值本身,而是返回最小值所在的位置(索引)

  • min(): 返回这些函数值里最小的那个数(在这个例子中接近 $0$)。

  • argmin(): 返回“第几个数是最小的”。

4. 为什么要在 Manim 里这么做?

这段代码的目的是为了找到抛物线的最低点,并让动画里的点运动到那里:

Python
 
# 1. 在 x 轴范围内取 200 个样点
x_space = np.linspace(0, 10, 200)

# 2. 算出这 200 个点对应的 y 值,并找到 y 最小的那个位置的编号
minimum_index = func(x_space).argmin() 

# 3. 通过编号找到对应的 x 值
min_x = x_space[minimum_index] 

# 4. 播放动画,让 tracker 移动到那个 x 值
self.play(t.animate.set_value(min_x))

总结 (TL;DR)

  • func 只是一个普通的 Python 函数。

  • 因为输入的是 NumPy 数组,所以输出也是 NumPy 数组

  • .argmin()NumPy 数组自带的绝招,用来精准定位最小值的“坐标索引”。

这其实就是一种简单的数值优化寻找最小值的方法,在 Manim 中处理数学动态演示非常实用!

 

posted on 2026-05-15 20:04  ZhangZhihuiAAA  阅读(25)  评论(0)    收藏  举报