This tool presents four geometric views of the matrix product .
Choose the matrix size ( or ), enter the matrix entries, switch between views, and follow the computation step by step.
How to Use the Tool¶
| Control | Function |
|---|---|
| 2×2 / 3×3 buttons | Switch the matrix size and load the default example automatically |
| Matrix entry boxes | Edit any entry of or directly; the figure updates immediately |
| View selector | Switch among the four views; each view highlights the rows/columns involved in the computation in different colors |
| ← → | Step through the computation of the current view |
| Computation panel | Shows the full formula for the current step |
Highlight colors: blue border = the row taking part in the inner product, the coefficient vector of the column combination, and the rows of in the row combination and the outer-product expansion; violet border = the column taking part in the inner product, the coefficient vector of the row combination, and the columns of in the column combination and the outer-product expansion; teal border = the position of the result of the current step.
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import ipywidgets as widgets
from IPython.display import display, HTML
# --- Color scheme (Accent Mix) ---
C_BG = "#F8F8F8"
C_GRID = "#D6D6D6"
C_AXIS = "#000000"
C_V1 = "#57068C" # violet
C_V2 = "#006385" # deep blue
C_T1 = "#2AD2C9" # teal
C_T2 = "#8900E1" # Ultra Violet
C_WARN = "#FF5D47" # orange
C_AUX = "#AB82C5" # light violet
# Semi-transparent fill colors (RGBA)
C_HL_ROW = "rgba(0,99,133,0.25)" # row highlight — deep blue
C_HL_COL = "rgba(87,6,140,0.25)" # column highlight — violet
C_HL_RESULT = "rgba(42,210,201,0.25)" # result highlight — Teal
C_HL_ACTIVE = "rgba(255,93,71,0.20)" # current focus — Orange
C_NEUTRAL = "rgba(255,255,255,0.9)" # no highlight
BORDER_ROW = C_V2
BORDER_COL = C_V1
BORDER_RESULT = C_T1
BORDER_ACTIVE = C_WARN
BORDER_NEUTRAL = C_GRID
FONT_FAMILY = "'Noto Sans TC', 'Microsoft JhengHei', 'SimHei', Arial, sans-serif"
print("✓ Color constants loaded")✓ Color constants loaded
# ── Utility functions ──────────────────────────────────────────────────────────────────
def make_mat_trace(mat, highlights, x_offset=0, cell_size=1.0, label=""):
"""
Convert the matrix mat into Plotly shapes + annotations.
highlights: dict { (i,j): ('fill_color', 'border_color') }
x_offset: x coordinate of the left edge of the matrix
"""
rows, cols = mat.shape
shapes, annotations = [], []
for i in range(rows):
for j in range(cols):
fill, border = highlights.get((i, j), (C_NEUTRAL, BORDER_NEUTRAL))
x0 = x_offset + j * cell_size
y0 = (rows - 1 - i) * cell_size
shapes.append(dict(
type="rect",
x0=x0, y0=y0,
x1=x0 + cell_size, y1=y0 + cell_size,
fillcolor=fill,
line=dict(color=border, width=2)
))
annotations.append(dict(
x=x0 + cell_size / 2,
y=y0 + cell_size / 2,
text=str(int(mat[i, j])) if mat[i, j] == int(mat[i, j]) else f"{mat[i,j]:.1f}",
showarrow=False,
font=dict(size=16, family=FONT_FAMILY,
color=border if border != BORDER_NEUTRAL else C_AXIS),
xanchor="center", yanchor="middle"
))
# Matrix label
if label:
annotations.append(dict(
x=x_offset + cols * cell_size / 2,
y=-0.55,
text=f"<b>{label}</b>",
showarrow=False,
font=dict(size=14, family=FONT_FAMILY, color=C_V1),
xanchor="center", yanchor="top"
))
return shapes, annotations
def make_operator_annotation(x, y, text, size=18):
return dict(
x=x, y=y, text=f"<b>{text}</b>",
showarrow=False,
font=dict(size=size, family=FONT_FAMILY, color=C_AUX),
xanchor="center", yanchor="middle"
)
def layout_positions(n, cell_size=1.0, gap=0.7):
"""Compute the starting x positions of A, ×, B, =, C"""
mat_w = n * cell_size
xA = 0
x_mul = mat_w + gap * 0.35
xB = mat_w + gap
x_eq = 2 * mat_w + gap + gap * 0.35
xC = 2 * mat_w + 2 * gap
return xA, x_mul, xB, x_eq, xC
print("✓ Utility functions defined")✓ Utility functions defined
# ── Highlighting logic of the four views ────────────────────────────────────────────────────────────
def highlights_inner_product(n, step):
"""View 1: inner product. step = i*n + j; highlight row i of A and column j of B; result C[i,j]"""
total = n * n
i, j = divmod(step % total, n)
hA = {(i, k): (C_HL_ROW, BORDER_ROW) for k in range(n)}
hB = {(k, j): (C_HL_COL, BORDER_COL) for k in range(n)}
hC = {(i, j): (C_HL_RESULT, BORDER_RESULT)}
return hA, hB, hC, i, j
def highlights_col_combo(n, step):
"""View 2: column combination. step = j; highlight all of A, column j of B, column j of C"""
j = step % n
hA = {(i, k): (C_HL_COL, BORDER_COL) for i in range(n) for k in range(n)}
hB = {(k, j): (C_HL_ROW, BORDER_ROW) for k in range(n)}
hC = {(i, j): (C_HL_RESULT, BORDER_RESULT) for i in range(n)}
return hA, hB, hC, -1, j
def highlights_row_combo(n, step):
"""View 3: row combination. step = i; highlight row i of A, all of B, row i of C"""
i = step % n
hA = {(i, k): (C_HL_COL, BORDER_COL) for k in range(n)}
hB = {(k, j): (C_HL_ROW, BORDER_ROW) for k in range(n) for j in range(n)}
hC = {(i, j): (C_HL_RESULT, BORDER_RESULT) for j in range(n)}
return hA, hB, hC, i, -1
def highlights_outer(n, step):
"""View 4: outer-product expansion. step = k; highlight column k of A, row k of B, all of C (accumulated)"""
k = step % n
hA = {(i, k): (C_HL_COL, BORDER_COL) for i in range(n)}
hB = {(k, j): (C_HL_ROW, BORDER_ROW) for j in range(n)}
hC = {(i, j): (C_HL_RESULT, BORDER_RESULT) for i in range(n) for j in range(n)}
return hA, hB, hC, k, k
VIEW_FUNCS = [
highlights_inner_product,
highlights_col_combo,
highlights_row_combo,
highlights_outer,
]
VIEW_STEPS = lambda n: [n*n, n, n, n] # number of steps of each view
print("✓ Highlighting logic of the four views defined")✓ Highlighting logic of the four views defined
# ── Formula text ──────────────────────────────────────────────────────────────
def formula_inner(A, B, C, n, step):
i, j = divmod(step, n)
terms = " + ".join([f"{int(A[i,k])}×{int(B[k,j])}" for k in range(n)])
return (
f"<b style='color:{C_V1}'>View 1: Inner product</b><br>"
f"Compute (AB)[{i},{j}]<br>"
f"= row {i} of A · column {j} of B<br>"
f"= {terms}<br>"
f"= <b style='color:{C_T1}'>{int(C[i,j])}</b>"
)
def formula_col_combo(A, B, C, n, step):
j = step
col_b = [int(B[k, j]) for k in range(n)]
lines = []
for i in range(n):
terms = " + ".join([f"{col_b[k]}·{int(A[i,k])}" for k in range(n)])
lines.append(f"(AB)[{i},{j}] = {terms} = <b>{int(C[i,j])}</b>")
body = "<br>".join(lines)
col_str = "[" + ", ".join(map(str, col_b)) + "]^T"
return (
f"<b style='color:{C_V1}'>View 2: Column combination</b><br>"
f"column {j} of AB = combination of the columns of A with coefficients B[:,{j}]={col_str}<br>"
f"{body}"
)
def formula_row_combo(A, B, C, n, step):
i = step
row_a = [int(A[i, k]) for k in range(n)]
lines = []
for j in range(n):
terms = " + ".join([f"{row_a[k]}·{int(B[k,j])}" for k in range(n)])
lines.append(f"(AB)[{i},{j}] = {terms} = <b>{int(C[i,j])}</b>")
body = "<br>".join(lines)
row_str = "[" + ", ".join(map(str, row_a)) + "]"
return (
f"<b style='color:{C_V1}'>View 3: Row combination</b><br>"
f"row {i} of AB = combination of the rows of B with coefficients A[{i},:]={row_str}<br>"
f"{body}"
)
def formula_outer(A, B, C, n, step):
k = step
col_a = np.array([[A[i, k]] for i in range(n)])
row_b = np.array([B[k, :]])
rank1 = col_a @ row_b
partial = sum(np.array([[A[i,kk]] for i in range(n)]) @ np.array([B[kk,:]]) for kk in range(k+1))
r1_str = " ".join(["[" + ", ".join(map(lambda x: str(int(x)), rank1[i])) + "]" for i in range(n)])
col_a_str = "[" + ", ".join([str(int(A[i,k])) for i in range(n)]) + "]^T"
row_b_str = "[" + ", ".join([str(int(B[k,j])) for j in range(n)]) + "]"
partial_str = " ".join(["[" + ", ".join([str(int(partial[i,j])) for j in range(n)]) + "]" for i in range(n)])
return (
f"<b style='color:{C_V1}'>View 4: Outer-product expansion</b><br>"
f"k={k}: rank-one matrix = col{k}(A) × row{k}(B)<br>"
f"= <span style='color:{C_V1}'>{col_a_str}</span> × <span style='color:{C_V2}'>{row_b_str}</span><br>"
f"= {r1_str}<br>"
f"<b>Accumulated (k=0..{k}):</b> {partial_str}"
)
FORMULA_FUNCS = [formula_inner, formula_col_combo, formula_row_combo, formula_outer]
print("✓ Formula text functions defined")✓ Formula text functions defined
# ── Main plotting function ────────────────────────────────────────────────────────────────
def build_figure(A, B, view_idx, step, cell_size=1.0):
n = A.shape[0]
C = A @ B
gap = 0.8 if n == 2 else 0.65
xA, x_mul, xB, x_eq, xC = layout_positions(n, cell_size, gap)
hA, hB, hC, *_ = VIEW_FUNCS[view_idx](n, step)
sA, annA = make_mat_trace(A, hA, x_offset=xA, cell_size=cell_size, label="A")
sB, annB = make_mat_trace(B, hB, x_offset=xB, cell_size=cell_size, label="B")
sC, annC = make_mat_trace(C, hC, x_offset=xC, cell_size=cell_size, label="AB")
op_y = (n * cell_size) / 2 - cell_size * 0.05
ann_ops = [
make_operator_annotation(x_mul + 0.15, op_y, "×"),
make_operator_annotation(x_eq + 0.15, op_y, "="),
]
formula_html = FORMULA_FUNCS[view_idx](A, B, C, n, step)
total_w = xC + n * cell_size + 0.2
fig_h = max(n * cell_size + 1.2, 2.5)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=[0], y=[0], mode='markers',
marker=dict(opacity=0),
showlegend=False, hoverinfo='none'
))
fig.update_layout(
shapes=sA + sB + sC,
annotations=annA + annB + annC + ann_ops,
plot_bgcolor=C_BG,
paper_bgcolor="white",
xaxis=dict(
range=[-0.1, total_w],
showgrid=False, zeroline=False, showticklabels=False,
scaleanchor="y", scaleratio=1
),
yaxis=dict(
range=[-0.85, n * cell_size + 0.15],
showgrid=False, zeroline=False, showticklabels=False
),
margin=dict(l=10, r=10, t=10, b=10),
height=int(fig_h * 80 + 60),
width=None,
font=dict(family=FONT_FAMILY),
)
return fig, formula_html
print("✓ Main plotting function defined")✓ Main plotting function defined
# ── Default matrices ──────────────────────────────────────────────────────────────────
DEFAULT_A2 = np.array([[1, 2], [0, 3]], dtype=float)
DEFAULT_B2 = np.array([[2, 1], [1, 4]], dtype=float)
DEFAULT_A3 = np.array([[1, 2, 0], [3, 1, 2], [0, 1, 4]], dtype=float)
DEFAULT_B3 = np.array([[2, 0, 1], [1, 3, 2], [0, 2, 1]], dtype=float)
print("Default matrices (2×2):")
print(f"A =\n{DEFAULT_A2}")
print(f"B =\n{DEFAULT_B2}")
print(f"AB =\n{DEFAULT_A2 @ DEFAULT_B2}")
print()
print("Default matrices (3×3):")
print(f"A =\n{DEFAULT_A3}")
print(f"B =\n{DEFAULT_B3}")
print(f"AB =\n{DEFAULT_A3 @ DEFAULT_B3}")Default matrices (2×2):
A =
[[1. 2.]
[0. 3.]]
B =
[[2. 1.]
[1. 4.]]
AB =
[[ 4. 9.]
[ 3. 12.]]
Default matrices (3×3):
A =
[[1. 2. 0.]
[3. 1. 2.]
[0. 1. 4.]]
B =
[[2. 0. 1.]
[1. 3. 2.]
[0. 2. 1.]]
AB =
[[ 4. 6. 5.]
[ 7. 7. 7.]
[ 1. 11. 6.]]
# ── ipywidgets interactive interface ───────────────────────────────────────────────────────
VIEW_NAMES = ["View 1: Inner product", "View 2: Column combination", "View 3: Row combination", "View 4: Outer-product expansion"]
VIEW_DESC = [
"(AB)ᵢⱼ = inner product of row i of A and column j of B",
"each column of AB is a linear combination of the columns of A, with the corresponding column of B as coefficients",
"each row of AB is a linear combination of the rows of B, with the corresponding row of A as coefficients",
"AB = Σₖ col_k(A) · row_k(B); each term is a rank-one matrix, and their sum recovers the full product",
]
# ── State ──
state = {
'n': 2,
'view': 0,
'step': 0,
'A': DEFAULT_A2.copy(),
'B': DEFAULT_B2.copy(),
}
# ── Size selection ──
dim_toggle = widgets.ToggleButtons(
options=[('2×2', 2), ('3×3', 3)],
value=2,
button_style='',
style={'button_width': '70px', 'font_weight': 'bold'},
description='Size:'
)
# ── View selection ──
view_toggle = widgets.ToggleButtons(
options=[(n, i) for i, n in enumerate(VIEW_NAMES)],
value=0,
button_style='',
style={'button_width': '170px'},
description='View:'
)
# ── Matrix input ──
def make_mat_inputs(n, mat, label_prefix):
cells = []
for i in range(n):
row = []
for j in range(n):
w = widgets.FloatText(
value=mat[i, j],
layout=widgets.Layout(width='54px', height='36px'),
style={'description_width': '0px'}
)
row.append(w)
cells.append(row)
return cells
cells_A = make_mat_inputs(2, DEFAULT_A2, 'A')
cells_B = make_mat_inputs(2, DEFAULT_B2, 'B')
def cells_to_mat(cells):
return np.array([[c.value for c in row] for row in cells], dtype=float)
def mat_grid(cells, label):
rows = [widgets.HBox(row) for row in cells]
title = widgets.HTML(f"<b style='color:{C_V1};font-size:15px'>{label}</b>")
return widgets.VBox([title] + rows,
layout=widgets.Layout(margin='0 16px 0 0'))
# ── Step navigation ──
btn_prev = widgets.Button(description='← Back',
layout=widgets.Layout(width='90px'),
button_style='warning')
btn_next = widgets.Button(description='Next →',
layout=widgets.Layout(width='90px'),
button_style='primary')
step_label = widgets.HTML()
# ── Output areas ──
fig_out = widgets.Output()
formula_out = widgets.Output()
desc_out = widgets.HTML()
# ── Rebuild the matrix entry boxes ──
mat_input_area = widgets.HBox([])
def rebuild_mat_inputs():
global cells_A, cells_B
n = state['n']
cells_A = make_mat_inputs(n, state['A'], 'A')
cells_B = make_mat_inputs(n, state['B'], 'B')
for row in cells_A:
for c in row:
c.observe(on_cell_change, names='value')
for row in cells_B:
for c in row:
c.observe(on_cell_change, names='value')
mat_input_area.children = [
mat_grid(cells_A, 'A'),
widgets.HTML(f"<span style='font-size:22px;color:{C_AUX};line-height:3.2'>×</span>"),
mat_grid(cells_B, 'B'),
]
# ── Rendering ──
def render():
v = state['view']
s = state['step']
n = state['n']
A = state['A']
B = state['B']
total = VIEW_STEPS(n)[v]
s = min(s, total - 1)
state['step'] = s
btn_prev.disabled = (s == 0)
btn_next.disabled = (s == total - 1)
step_label.value = f"<span style='font-size:13px;color:gray'>Step {s+1} / {total}</span>"
desc_out.value = (
f"<div style='padding:8px 12px;background:#f0eaf7;"
f"border-left:4px solid {C_V1};border-radius:6px;"
f"font-size:13px;color:#333;margin-bottom:8px'>"
f"<b style='color:{C_V1}'>{VIEW_NAMES[v]}</b> {VIEW_DESC[v]}</div>"
)
fig, formula_html = build_figure(A, B, v, s)
with fig_out:
fig_out.clear_output(wait=True)
fig.show(config={'displayModeBar': False, 'responsive': True})
with formula_out:
formula_out.clear_output(wait=True)
display(HTML(
f"<div style='padding:12px 16px;background:{C_BG};"
f"border-radius:8px;border-left:4px solid {C_T1};"
f"font-size:14px;line-height:1.85;font-family:{FONT_FAMILY}'>"
f"{formula_html}</div>"
))
# ── Event handling ──
def on_dim_change(change):
if change['name'] != 'value': return
n = change['new']
state['n'] = n
state['step'] = 0
state['A'] = (DEFAULT_A2 if n == 2 else DEFAULT_A3).copy()
state['B'] = (DEFAULT_B2 if n == 2 else DEFAULT_B3).copy()
rebuild_mat_inputs()
render()
def on_view_change(change):
if change['name'] != 'value': return
state['view'] = change['new']
state['step'] = 0
render()
def on_cell_change(change):
state['A'] = cells_to_mat(cells_A)
state['B'] = cells_to_mat(cells_B)
render()
def on_prev(b):
state['step'] = max(0, state['step'] - 1)
render()
def on_next(b):
n = state['n']; v = state['view']
state['step'] = min(VIEW_STEPS(n)[v] - 1, state['step'] + 1)
render()
dim_toggle.observe(on_dim_change, names='value')
view_toggle.observe(on_view_change, names='value')
btn_prev.on_click(on_prev)
btn_next.on_click(on_next)
# ── Layout ──
rebuild_mat_inputs()
# Initialize the cell observers
for row in cells_A:
for c in row:
c.observe(on_cell_change, names='value')
for row in cells_B:
for c in row:
c.observe(on_cell_change, names='value')
ui = widgets.VBox([
widgets.HTML(f"<h3 style='color:{C_V1};margin-bottom:4px'>Four Views of Matrix Multiplication: A Comparison Tool</h3>"),
widgets.HBox([dim_toggle]),
widgets.HTML("<hr style='margin:8px 0;border-color:#ddd'>"),
widgets.HTML(f"<b style='font-size:13px;color:gray'>Enter the matrix entries:</b>"),
mat_input_area,
widgets.HTML("<hr style='margin:8px 0;border-color:#ddd'>"),
view_toggle,
desc_out,
widgets.HBox([btn_prev, step_label, btn_next],
layout=widgets.Layout(align_items='center', gap='10px')),
fig_out,
widgets.HTML(f"<b style='font-size:13px;color:{C_V1}'>Computation:</b>"),
formula_out,
], layout=widgets.Layout(padding='8px', max_width='860px'))
display(ui)
render()
print("✓ Interactive interface started")✓ Interactive interface started
Part 2: Observation and Reflection¶
Answer the following four questions, explaining your conclusions with what you actually did in the interactive tool.
We suggest switching to the 3×3 size first and then entering the matrices as each question instructs.
Question 1: What Is Special about the Identity Matrix¶
Set to the identity matrix
Keep an arbitrary matrix, and follow the computation of step by step in each of the four views.
(a) (Inner-product view) When is computed, what does column of contribute to the inner product? Why does every equal the corresponding entry of ?
(b) (Column-combination view) Each column of is a linear combination of the columns of ; look at the coefficient vectors. What is special about the coefficient vectors when is the identity matrix?
(c) (Outer-product expansion view) Three rank-one matrices appear one after another and are added up, and the final result is itself. Describe the structure of each rank-one matrix, and explain why the sum of the three rank-one matrices recovers exactly .
(d) Putting these observations together, explain in one sentence: why does hold for every matrix ?
Question 2: The Equivalence of the Views¶
Take the size and set
First use the inner-product view to compute all 9 entries of step by step, and record the results.
Then switch to the row-combination view and look at the computation of row 0.
(a) When the inner-product view computes , which row of and which column of are highlighted? Write out the formula.
(b) Switch to the row-combination view; step 0 shows row 0 of . Look at the formula panel: it writes row 0 as a linear combination of the rows of with coefficients . Expand the formula and verify that agrees with the result of (a).
(c) Switch to the column-combination view; step 0 shows column 0 of . Expand the formula again and verify that is still the same.
(d) The four views compute the same result matrix. Starting from this observation, explain the relationship between “the variety of ways to define” matrix multiplication and “the uniqueness of the result.”
Question 3: The Outer-Product Expansion and the “Rank-One Decomposition” of a Matrix¶
Switch to the size, use the outer-product expansion view, enter each of the following two pairs of matrices, and watch the three rank-one matrices pile up.
Case I:
Case II:
(a) In Case I, what is the rank-one matrix for ? How is it related to row 0 of ?
(b) In Case II, look at the three rank-one matrices: which of them are zero matrices? Why? (Hint: look at columns 1 and 2 of .)
(c) What is the final product in Case II? Explain it directly from the outer-product expansion, without computing.
(d) Based on these observations, complete the statement: “If column of is entirely zero, then the -th term of the outer-product expansion ________, so computing is equivalent to using only the ________ terms.”
Question 4: The Noncommutativity of Matrix Multiplication¶
With the size, compute each of the following two products, and observe them in at least two different views.
(a) First compute (the default order). Switch to the column-combination view and describe how column 0 of is formed from the columns of .
(b) Swap and (enter the original into the boxes for , and vice versa) and compute . Switch to the row-combination view and describe how the coefficients of the combination for row 0 of differ from those for row 0 of .
(c) Record the results for and , and confirm that .
Then try the diagonal matrices , . Does equal this time?
(d) Using the column-combination and row-combination views, explain why in general: the former says “each column of is a combination of the columns of ,” and the latter says “each row of is a combination of the rows of .” Which matrix supplies the coefficients in each case?