# 教材位置：第6章，第6.2节；图 6-1。
# 单位、变量与形状：a、b、c、p、r 均为 (2,)；坐标使用统一长度单位，内积按长度平方计。
# 学习目标：计算二维向量的线性组合、内积、欧氏长度和正交投影，
# 并用图形与勾股关系理解投影残差。
# 核心数学关系：proj_a(c)=((c·a)/(a·a))*a，residual=c-proj_a(c)，
# 且 a·residual=0、||c||^2=||proj_a(c)||^2+||residual||^2（要求 a 非零）。
# 数据与输出：使用两个二维向量及其线性组合，打印和、内积、长度、
# 投影向量和残差，保存 e20_vector.png。
# 运行方式：安装 NumPy、Matplotlib 后，运行 python e20_vector.py；
# 图片写入运行命令时的当前工作目录。

import numpy as np
import matplotlib.pyplot as plt
# 在创建第一幅图之前配置可跨系统选择的中文字体。
from plot_style import configure_chinese_plot

configure_chinese_plot()
# a、b 都是形状 (2,) 的一维数组，按两个坐标表示向量；
# 它们没有显式的“行向量/列向量”第二维，@ 在这里执行向量内积。
a = np.array([2.0, 1.0])
b = np.array([-1.0, 2.0])
# 线性组合逐坐标进行：c=1.5*a-0.5*b=[3.5,0.5]。
# 系数的正负决定沿该向量同向或反向移动，绝对值决定伸缩比例。
c = 1.5 * a - 0.5 * b
# c@a 是内积，a@a=||a||^2；这个比值是投影沿 a 的伸缩系数。
# projection 是二维投影向量，并不是一个“投影长度”标量；
# 沿单位方向 a/||a|| 的有符号标量分量为 (c@a)/||a||，
# 投影向量的长度则为 abs(c@a)/||a||。此处 a 非零，分母有定义。
projection = (c @ a) / (a @ a) * a
# residual 是从投影点指向 c 的向量；正交投影使这部分与 a 垂直。
residual = c - projection
print("向量和 a+b =", a + b, "；线性组合 c =", c)
# norm(a) 默认求欧氏长度 sqrt(a@a)；round 仅调整显示精度。
print("内积 aᵀb =", a @ b, "；二范数 ‖a‖₂ =", round(np.linalg.norm(a), 6))
print("投影向量 =", projection, "；残差向量 =", residual)
# allclose 用来比较向量各分量，isclose 用来比较标量，允许浮点舍入误差。
assert np.allclose(c, [3.5, 0.5])
# a 与 b 正交；同时验证残差确实与投影所在直线的方向 a 正交。
assert np.isclose(a @ b, 0) and np.isclose(a @ residual, 0)
# 向量与自身的内积是长度平方；此式就是正交分解后的勾股定理。
assert np.isclose(c @ c, projection @ projection + residual @ residual)
# 同一批已计算向量分成两个面板：左侧解释首尾相接，右侧解释正交分解。
# 两图直接使用 a、b、c、projection、residual，不另外输入一套向量数值。
fig, axes = plt.subplots(1, 2, figsize=(6.3, 3.1))
for ax in axes:
    # equal 让两个坐标轴单位长度一致，否则图上的直角与长度比例可能失真。
    ax.set_aspect("equal")
    ax.grid(alpha=0.3)
    ax.axhline(0, color="gray", lw=0.5)
    ax.axvline(0, color="gray", lw=0.5)
    ax.set(xlim=(-1.6, 4.4), ylim=(-0.5, 3), xlabel="$x_1$", ylabel="$x_2$")
# xy 是箭头终点，xytext 是起点；空字符串表示只画箭头，不附加正文。
# 向量平移不改变其分量；第二条组合箭头从 projection 走到 c，位移为 residual。
ax = axes[0]
for start, end, label, color in [
    (np.zeros(2), a, "$a$", "gray"),
    (np.zeros(2), b, "$b$", "gray"),
    (np.zeros(2), projection, "$1.5a$", "tab:green"),
    (projection, c, "$-0.5b$", "tab:orange"),
    (np.zeros(2), c, "$c$", "tab:blue"),
]:
    ax.annotate("", xy=end, xytext=start, arrowprops={"arrowstyle": "->",
        "color": color})
    # end[0]、end[1] 分别是横纵坐标；稍微偏移标签，避免遮住箭头尖端。
    if label == "$-0.5b$":
        ax.text(end[0] + 0.08, end[1] - 0.35, label, color=color)
    else:
        ax.text(end[0] + 0.08, end[1] + 0.08, label, color=color)
ax.set_title("向量的线性组合")
ax = axes[1]
# 每次取一个向量和对应标签，统一从原点画箭头，便于比较方向和长度。
for v, label, color in [(c, "$c$", "tab:blue"), (projection, "$p$", "tab:green")]:
    ax.annotate("", xy=v, xytext=(0, 0), arrowprops={"arrowstyle": "->",
        "color": color})
    ax.text(v[0] + 0.08, v[1] + 0.08, label, color=color)
# 红色虚线连接 projection 与 c，其位移正好是 residual；
# 残差箭头虽未从原点画出，但向量只取决于位移，平移不会改变它。
ax.plot([projection[0], c[0]], [projection[1], c[1]], "r--", label="残差 $r$")
ax.annotate("", xy=c, xytext=projection, arrowprops={"arrowstyle": "->",
    "color": "red"})
# 沿 a 和 residual 的单位方向画直角记号；本例两方向均非零且正交。
u = a / np.linalg.norm(a)
w = residual / np.linalg.norm(residual)
right_angle = np.array([projection - 0.18 * u,
                        projection - 0.18 * u + 0.18 * w,
                        projection + 0.18 * w])
ax.plot(*right_angle.T, color="gray", lw=0.8)
ax.set_title("向量的正交投影")
ax.text(0.02, 0.04, "正交投影 p；$a^Tr=0$", transform=ax.transAxes, fontsize=8)
ax.legend(fontsize=8, loc="upper left")
# 指定显示范围，保证所有箭头和标签位于图内。
# 调整布局后保存并关闭，避免批量作图时保留多余图对象。
fig.tight_layout()
fig.savefig("e20_vector.png", dpi=180, bbox_inches="tight")
plt.close(fig)
