# 教材位置：第6章，第6.4节；图 6-3。
# 单位、变量与形状：v 为 (2,)，X 为 (3,2)，row 为 (2,)；数组元素是无量纲教学数字。
# 学习目标：区分矩阵乘法与逐元素运算，理解一维数组的形状、广播规则，
# 并通过切片视图和 copy 学会判断数组是否共享底层数据。
# 核心规则：@ 按线性代数的求和规则相乘，* 按对应元素相乘；
# 广播从最右侧维度对齐，每一对维度须相等或其中之一为 1。
# 数据与输出：使用 2×2 矩阵、长度为 2 的向量和 3×2 数组，打印各种运算，
# 展示不兼容广播被拒绝，以及修改视图如何改变原数组；保存 e22_arrays.png。
# 运行方式：安装 NumPy 后，在终端运行 python e22_arrays.py。

import numpy as np
# A、B 的形状都是 (2,2)；B 是交换两个坐标的矩阵。
A = np.array([[1, 2], [3, 4]])
B = np.array([[0, 1], [1, 0]])
# v 的 shape 为 (2,)，只有一个轴，并不是形状 (1,2) 或 (2,1) 的二维数组。
v = np.array([1.0, 2.0])
# A@B 的 (i,j) 元素是 A 的第 i 行与 B 的第 j 列的内积；
# A*B 只把相同位置的数字相乘。本例右乘 B 会交换 A 的两列。
# sep="\n" 让本次 print 的多个参数分行显示，便于比较两种结果。
print("A@B =", A @ B, "; A*B =", A * B, sep="\n")
# A@v 计算各行与 v 的内积，等价于把 v 当列向量；
# v@A 计算 v 与各列的内积，等价于把 v 当行向量；结果均为形状 (2,)。
print("A@v =", A @ v, "; v@A =", v @ A)
# 一维数组转置仍为 (2,)；[:,None] 在第二轴新增长度为 1 的维度，
# 才得到真正的二维列形数组 (2,1)，其中 None 等价于 np.newaxis。
print("数组形状：", v.shape, v.T.shape, v[:, None].shape)
# array_equal 检查形状与元素完全一致，适合此处确定的整数计算。
assert np.array_equal(A @ B, [[2, 1], [4, 3]])
assert not np.array_equal(A @ B, A * B)
assert v.T.shape == (2,) and v[:, None].shape == (2, 1)
# X 形状 (3,2)，row 形状 (2,)；广播时将 row 视作 (1,2)，
# 第一轴的 1 可扩展为 3，相同的两项偏移便应用到每一行。
X = np.ones((3, 2))
row = np.array([10, 20])
print("X+row =", X + row)
# [[11,21]]*3 在这里是 Python 列表重复，构造三行相同的预期结果，
# 并不是 NumPy 数组的数值乘法；allclose 将其与计算结果逐元素比较。
assert np.allclose(X + row, [[11, 21]] * 3)
# 用一个预期失败的例子认识广播边界；try/except 捕获错误后程序可继续。
try:
    # arange(3) 的形状是 (3,)。从右对齐后遇到 X 的末维 2 与 3，
    # 二者不相等且都不是 1，所以无法相加；它不会自动猜测你想按列加。
    X + np.arange(3)
except ValueError:
    # NumPy 用 ValueError 报告形状不兼容；捕获到它说明此项自检符合预期。
    print("已拒绝不兼容的数组形状")
else:
    # try 没有抛错才会执行 else；若不兼容运算竟然成功，就主动报告自检失败。
    raise AssertionError("此形状的广播应失败")
# arange(6.0) 创建浮点数组 [0,1,2,3,4,5]。基本切片 [2:5] 包含下标 2、3、4，
# 得到与 base 共享内存的视图；view[0] 对应 base[2]，因此赋值会改动 base。
base = np.arange(6.0)
view = base[2:5]
view[0] = 99
# copy() 显式复制数据；separate 修改自己的第一个元素，不再影响 base。
separate = base[2:5].copy()
separate[0] = -1
# 同时确认原数组保留视图写入的 99，而副本可独立改成 -1。
assert base[2] == 99 and separate[0] == -1
print("修改视图、复制后，原数组 =", base)

# 配套图直接画上面实际使用的 v、X、row；这里不改 v 的长度，也不把加法换成减法。
# Matplotlib 只负责画图，不改变 NumPy 运算或上面已经打印的数值输出。
import matplotlib.pyplot as plt
# 在创建第一幅图之前配置可跨系统选择的中文字体。
from plot_style import configure_chinese_plot

configure_chinese_plot()
fig, axes = plt.subplots(1, 2, figsize=(6.3, 3.0))
ax = axes[0]
ax.axis("off")
# 用文本方框展示同一份 v 的三种数组形状；None 只插轴，不改变元素顺序。
for y_position, label, text in [
    (0.82, "$v$: (2,)", str(v)),
    (0.52, "$v[None,:]$: (1,2)", str(v[None, :])),
    (0.16, "$v[:,None]$: (2,1)", str(v[:, None])),
]:
    ax.text(0.02, y_position, label, transform=ax.transAxes, fontsize=9, va="center")
    ax.text(0.57, y_position, text, transform=ax.transAxes, fontsize=10,
            family="monospace", va="center", bbox={"facecolor": "#e7f0f7",
                "edgecolor": "#52758c"})
ax.set_title("同一数组的三种形状")
ax = axes[1]
ax.axis("off")
# X+row 中 row 形状为(2,)，广播到 X 的每一行；此处数值直接读取实际运算。
assert X.shape == (3, 2) and row.shape == (2,)
broadcast_result = X + row
ax.text(0.03, 0.78, "$X$: (3,2)\n" + str(X), family="monospace", fontsize=9,
        transform=ax.transAxes, va="center")
ax.text(0.03, 0.42, "$row$: (2,)\n" + str(row), family="monospace", fontsize=9,
        transform=ax.transAxes, va="center")
ax.text(0.56, 0.62, "$X+row$: (3,2)\n" + str(broadcast_result), family="monospace",
    fontsize=9,
        transform=ax.transAxes, va="center", bbox={"facecolor": "#e7f0f7",
            "edgecolor": "#52758c"})
# arange(3) 的尾轴3与X的尾轴2不兼容，图中保留已验证的异常规则。
ax.text(0.03, 0.12, "(3,2) + (3,)  ->  ValueError", transform=ax.transAxes, fontsize=8)
ax.set_title("广播加法：从末轴对齐")
fig.tight_layout()
fig.savefig("e22_arrays.png", dpi=180, bbox_inches="tight")
plt.close(fig)
