0% found this document useful (0 votes)
42 views35 pages

Nb3 (Optional)

The document provides Python code for working with vectors and linear algebra concepts like norms. Functions are defined for creating and manipulating vectors, computing norms, drawing vector diagrams, and pretty-printing vectors in LaTeX format. Examples demonstrate using the functions to generate and visualize random vectors.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
42 views35 pages

Nb3 (Optional)

The document provides Python code for working with vectors and linear algebra concepts like norms. Functions are defined for creating and manipulating vectors, computing norms, drawing vector diagrams, and pretty-printing vectors in LaTeX format. Examples demonstrate using the functions to generate and visualize random vectors.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 35

In [1]:

# Just for reference, this prints the currently version of Python


import sys
print(sys.version)

3.8.7 (default, Jan 25 2021, 11:14:52)


[GCC 5.5.0 20171010]

In [2]:
# Code for pretty-printing math notation
from IPython.display import display, Math, Latex, Markdown

def display_math(str_latex):
display(Markdown('${}$'.format(str_latex)))

# Demo:
display_math(r'x \in \mathcal{S} \implies y \in \mathcal{T}')

In [3]:
# Code for drawing diagrams involving vectors
import matplotlib.pyplot as plt
%matplotlib inline

DEF_FIGLEN = 4
DEF_FIGSIZE = (DEF_FIGLEN, DEF_FIGLEN)

def figure(figsize=DEF_FIGSIZE):
return plt.figure(figsize=figsize)

def multiplot_figsize(plot_dims, base_figsize=DEF_FIGSIZE):


return tuple([p*x for p, x in zip(plot_dims, base_figsize)])

def subplots(plot_dims, base_figsize=DEF_FIGSIZE, sharex='col', sharey='row', **


assert len(plot_dims) == 2, "Must define a 2-D plot grid."
multiplot_size = multiplot_figsize(plot_dims, base_figsize)
_, axes = plt.subplots(plot_dims[0], plot_dims[1],
figsize=multiplot_size[::-1],
sharex=sharex, sharey=sharey,
**kw_args)
return axes

def new_blank_plot(ax=None, xlim=(-5, 5), ylim=(-5, 5), axis_color='gray', title


if ax is None:
ax = plt.gca()
else:
plt.sca(ax)
ax.axis('equal')
if xlim is not None: ax.set_xlim(xlim[0], xlim[1])
if ylim is not None: ax.set_ylim(ylim[0], ylim[1])
if axis_color is not None:
ax.axhline(color=axis_color)
ax.axvline(color=axis_color)
if title is not None:
ax.set_title(title)
return ax

def draw_point2d(p, ax=None, marker='o', markersize=5, **kw_args):


assert len(p) == 2, "Point must be 2-D."
if ax is None: ax = plt.gca()
ax.plot(p[0], p[1], marker=marker, markersize=markersize,
**kw_args);

def draw_label2d(p, label, coords=False, ax=None, fontsize=14,


dp=(0.0, 0.1), horizontalalignment='center', verticalalignment=
**kw_args):
assert len(p) == 2, "Position must be 2-D."
if ax is None: ax = plt.gca()
text = '{}'.format(label)
if coords:
text += ' = ({}, {})'.format(p[0], p[1])
ax.text(p[0]+dp[0], p[1]+dp[1], text,
fontsize=fontsize,
horizontalalignment=horizontalalignment,
verticalalignment=verticalalignment,
**kw_args)

def draw_line2d(start, end, ax=None, width=1.0, color='black', alpha=1.0, **kw_a


assert len(start) == 2, "`start` must be a 2-D point."
assert len(end) == 2, "`end` must be a 2-D point."
if ax is None:
ax = plt.gca()
x = [start[0], end[0]]
y = [start[1], end[1]]
ax.plot(x, y, linewidth=width, color=color, alpha=alpha, **kw_args);

def draw_vector2d(v, ax=None, origin=(0, 0), width=0.15, color='black', alpha=1.


**kw_args):
assert len(v) == 2, "Input vector must be two-dimensional."
if ax is None:
ax = plt.gca()
ax.arrow(origin[0], origin[1], v[0], v[1],
width=width,
facecolor=color,
edgecolor='white',
alpha=alpha,
length_includes_head=True,
**kw_args);

def draw_vector2d_components(v, y_offset_sign=1, vis_offset=0.05, comp_width=1.5


assert len(v) == 2, "Vector `v` must be 2-D."
y_offset = y_offset_sign * vis_offset
draw_line2d((0, y_offset), (v[0], y_offset), width=comp_width, **kw_args)
draw_line2d((v[0], y_offset), v, width=comp_width, **kw_args)

def draw_angle(theta_start, theta_end, radius=1, center=(0, 0), ax=None, **kw_ar


from matplotlib.patches import Arc
if ax is None: ax = plt.gca()
arc = Arc(center, center[0]+2*radius, center[1]+2*radius,
theta1=theta_start, theta2=theta_end,
**kw_args)
ax.add_patch(arc)

def draw_angle_label(theta_start, theta_end, label=None, radius=1, center=(0, 0)


from math import cos, sin, pi
if ax is None: ax = plt.gca()
if label is not None:
theta_label = (theta_start + theta_end) / 2 / 360 * 2.0 * pi
p = (center[0] + radius*cos(theta_label),
center[1] + radius*sin(theta_label))
ax.text(p[0], p[1], label, **kw_args)

print("Ready!")

Matplotlib is building the font cache; this may take a moment.


Ready!

In [4]:
figure()
new_blank_plot();
In [5]:
# Define three points
a = (-2, 2)
b = (3.5, 1)
c = (0.5, -3)

# Draw a figure containing these points


figure()
new_blank_plot()
draw_point2d(a, color='blue'); draw_label2d(a, 'a', color='blue', coords=True)
draw_point2d(b, color='red'); draw_label2d(b, 'b', color='red', coords=True)
draw_point2d(c, color='green'); draw_label2d(c, 'c', color='green', coords=True)
vector()

In [6]:
def vector(*elems, dim=None):
"""Exercise: What does this function do?"""
if dim is not None:
if len(elems) > 0:
assert dim == len(elems), "Number of supplied elements differs from
else: # No supplied elements
elems = [0.0] * dim
return tuple(elems)

def dim(v):
"""Returns the dimensionality of the vector `v`"""
return len(v)

v = vector(1.0, 2.0)
d = dim(v)
print('v = {} <== {}-dimensional'.format(v, d))

v = (1.0, 2.0) <== 2-dimensional

In [7]:
# Another example: Creates a zero-vector of dimension 3
z3 = vector(dim=3)
print('z3 = {} <== {}-dimensional'.format(z3, dim(z3)))

z3 = (0.0, 0.0, 0.0) <== 3-dimensional

print()
In [8]:
def latex_vector(v, transpose=False):
"""Returns a LaTeX string representation of a vector"""
s = r'''\left[ \begin{matrix} '''
sep = r'''\\''' if not transpose else r''', &'''
s += (r' {} ').format(sep).join([str(vi) for vi in v])
s += r''' \end{matrix}\right]'''
return s

# Demo: Pretty-print `v` from before


print("Standard Python output:", v)
print("\n'Mathy' output:")
v_latex = latex_vector(v)
display_math('v \equiv ' + v_latex)

Standard Python output: (1.0, 2.0)

'Mathy' output:

In [9]:
def length(v):
from math import sqrt
return sqrt(sum([vi*vi for vi in v]))

print("The length of v = {} is about {}.".format(v, length(v)))

The length of v = (1.0, 2.0) is about 2.23606797749979.


In [10]: figure()
new_blank_plot(title='Vector v = {}'.format(str(v)))
draw_vector2d(v, color='blue')

In [11]:
def random_vector(dim=2, v_min=-1, v_max=1):
"""Returns a random vector whose components lie in (`v_min`, `v_max`)."""
from random import uniform
v = vector(*[uniform(v_min, v_max) for _ in range(dim)])
return v

def flip_signs_randomly(v):
from random import choice
return [choice([-1, 1])*vi for vi in v]

# Draw `v` at the origin


subfigs = subplots((1, 2))
new_blank_plot(subfigs[0], title='v, placed at the origin')
draw_vector2d(v, color='blue')

# Draw `v` somewhere else


dv = flip_signs_randomly(random_vector(dim=dim(v), v_min=1, v_max=3))
new_blank_plot(subfigs[1], title='v, placed at ({:.1f}, {:.1f})'.format(dv[0], d
draw_vector2d(v, color='blue', origin=dv)
In [12]:
def norm(v, p=2):
assert p > 0
from math import sqrt, inf, pow
if p == 1: return sum([abs(vi) for vi in v])
if p == 2: return sqrt(sum([vi*vi for vi in v]))
if p == inf: return max([abs(vi) for vi in v])
return pow(sum([pow(abs(vi), p) for vi in v]), 1.0/p)

def latex_norm(x, p=2):


from math import inf
if p == inf: p = r'\infty'
s = r'\left\| '
s += x
s += r' \right\|_{}'.format(p)
return s

import math
for p in [1, 2, math.inf]:
v_pnorm_latex = latex_norm(v_latex, p)
display_math(r'{} \approx {}'.format(v_pnorm_latex, norm(v, p)))
In [13]:
from math import inf, sqrt

def normalize_vector(v, p=2):


"""Returns a rescaled version of the input vector `v`."""
v_norm = norm(v, p=p)
return vector(*[vi/v_norm for vi in v])

# Generate random points whose 2-norm equals 1. Then


# compute the 1-norm and inf-norm of these points.
norms_1 = [None] * 250
norms_inf = [None] * 250
for k in range(len(norms_1)):
v = normalize_vector(random_vector())
norms_1[k] = norm(v, p=1)
norms_inf[k] = norm(v, p=inf)

figure(figsize=(6, 6))
new_blank_plot(xlim=None, ylim=None, axis_color=None, title='$\|v\|_2 = 1$')
plt.plot(norms_1, norms_inf, marker='o', markersize=2, linestyle='none')
plt.xlabel('$\|v\|_1$', fontsize=18);
plt.ylabel('$\|v\|_\infty$', fontsize=18);
plt.hlines(y=1/sqrt(2), xmin=1, xmax=sqrt(2), linestyle=':')
plt.vlines(x=sqrt(2), ymin=1/sqrt(2), ymax=1, linestyle=':')
plt.axis('square');
In [14]:
from math import inf

figure(figsize=(6, 6))
new_blank_plot(xlim=(-1.25, 1.25), ylim=(-1.25, 1.25))

for p, color in zip([1, 2, inf], ['blue', 'green', 'red']):


print("Points whose {}-norm equals 1 are shown in {}.".format(p, color))
for _ in range(250):
v = normalize_vector(random_vector(), p=p)
# The `p`-norm of `v` is now equal to 1; plot `v`.
draw_point2d(v, color=color)

Points whose 1-norm equals 1 are shown in blue.


Points whose 2-norm equals 1 are shown in green.
Points whose inf-norm equals 1 are shown in red.
In [15]:
def scale(v, sigma):
return tuple([sigma*vi for vi in v])

va = vector(3.0, 2.0)
sigma = 0.75
va_scaled = scale(va, sigma)

va_latex = latex_vector(va)
va_scaled_latex = latex_vector(va_scaled)
display_math(r'''(\sigma={}) {} = {}'''.format(sigma, va_latex, va_scaled_latex)

axes = subplots((1, 2))


new_blank_plot(axes[0], xlim=(-1, 3.25), ylim=(-1, 3.25), title='blue')
draw_vector2d(va, color='blue')

new_blank_plot(axes[1], xlim=(-1, 3.25), ylim=(-1, 3.25), title='sigma * blue')


draw_vector2d(va_scaled, color='blue')

In [16]:
def add(v, w):
assert len(v) == len(w), "Vectors must have the same length."
return tuple([vi+wi for vi, wi in zip(v, w)])

vb = vector(-1.5, 1.0)
vc = add(va, vb)

vb_latex = latex_vector(vb)
vc_latex = latex_vector(vc)
display_math('{} + {} = {}'.format(va_latex, vb_latex, vc_latex))

In [17]:
axes = subplots((1, 3))
new_blank_plot(ax=axes[0], title='blue, red');
draw_vector2d(va, color='blue')
draw_vector2d(vb, color='red')

new_blank_plot(ax=axes[1], title='black = blue + red');


draw_vector2d(va, color='blue')
draw_vector2d(vb, origin=va, color='red', alpha=0.5)
draw_vector2d(vc)

new_blank_plot(ax=axes[2], title='black = red + blue');


draw_vector2d(vb, color='red', alpha=0.5)
draw_vector2d(va, origin=vb, color='blue', alpha=0.5)
draw_vector2d(vc)

In [18]:
def neg(v):
return tuple([-vi for vi in v])

def sub(v, w):


return add(v, neg(w))

vd = sub(va, vb)

vb_neg_latex = latex_vector(neg(vb))
vd_latex = latex_vector(vd)
display_math('{} + {} = {}'.format(va_latex, vb_neg_latex, vd_latex))

axes = subplots((1, 2))


new_blank_plot(ax=axes[0], title='blue, green');
draw_vector2d(va, color='blue')
draw_vector2d(vb, color='green')

new_blank_plot(ax=axes[1], title='black = blue - green');


draw_vector2d(va, color='blue')
draw_vector2d(neg(vb), origin=va, color='green', alpha=0.5)
draw_vector2d(vd)

In [19]:
ve = add(va_scaled, vb)

ve_latex = latex_vector(ve)
display_math(r'''{} {} + {} = {}'''.format(sigma, va_latex, vb_latex, ve_latex))

axes = subplots((1, 3))


new_blank_plot(axes[0], title='blue, red')
draw_vector2d(va, color='blue')
draw_vector2d(vb, color='red')

new_blank_plot(axes[1], title='sigma * blue')


draw_vector2d(va_scaled, color='blue', alpha=0.5)

new_blank_plot(axes[2], title='black = sigma*blue + red')


draw_vector2d(va_scaled, color='blue', alpha=0.5)
draw_vector2d(vb, origin=va_scaled, color='red', alpha=0.5)
draw_vector2d(ve)
In [20]:
def dot(u, w):
return sum([ui*wi for ui, wi in zip(u, w)])

In [21]:
u = (1, 2.5)
w = (3.25, 1.75)

display_math('u = ' + latex_vector(u))


display_math('w = ' + latex_vector(w))
u_dot_w_sum_long_latex = '+'.join([r'({}\cdot{})'.format(ui, wi) for ui, wi in z
display_math(r'\langle u, w \rangle = ' + u_dot_w_sum_long_latex + ' = ' + str(d
In [22]:
display_math('v^T = ' + latex_vector(v, transpose=True))

In [23]:
u_dot_w_vec_latex = latex_vector(u, transpose=True) + r' \cdot ' + latex_vector(
display_math(r'\langle u, w \rangle = u^T w = ' + u_dot_w_vec_latex + ' = ' + u_

In [24]:
# Write your code examples here

In [25]:
def radians_to_degrees(radians):
from math import pi
return radians * (180.0 / pi)
def get_angle_degrees(point):
assert len(point) == 2, "Point must be 2-D."
from math import pi, atan2
return radians_to_degrees(atan2(point[1], point[0]))

figure((6, 6))
new_blank_plot(xlim=(-0.5, 4), ylim=(-0.5, 4));

draw_vector2d(u, color='blue', width=0.05)


plt.text(u[0], u[1], '$(u_x, u_y) = ({}, {})$'.format(u[0], u[1]), color='blue',
horizontalalignment='center', verticalalignment='bottom', fontsize=14)
draw_vector2d(w, color='green', width=0.05)
plt.text(w[0], w[1], '$(w_x, w_y) = ({}, {})$'.format(w[0], w[1]), color='green'
horizontalalignment='center', verticalalignment='bottom', fontsize=14)

draw_vector2d_components(u, y_offset_sign=1, color='blue', linestyle='dashed')


draw_vector2d_components(w, y_offset_sign=-1, color='green', linestyle='dashed')

phi_degrees = get_angle_degrees(w)
theta_degrees = get_angle_degrees(u) - phi_degrees
draw_angle(0, phi_degrees, radius=1.5, color='green')
draw_angle_label(0, phi_degrees, r'$\phi$', radius=1.6, color='green', fontsize=
draw_angle(phi_degrees, phi_degrees+theta_degrees, radius=2, color='blue')
draw_angle_label(phi_degrees, phi_degrees+theta_degrees, r'$\theta$', radius=2.1
scale()
In [26]:
# Sample solution; how would you have done it?
def avgpairs(v):
assert dim(v) % 2 == 0, "Input vector `v` must be of even dimension."
v_pairs = zip(v[:-1:2], v[1::2])
v_avg = [0.5*(ve + vo) for ve, vo in v_pairs]
return vector(*v_avg)

v_pairs = vector(1, 2, 3, 4, 5, 6, 7, 8)
print(v_pairs, "=>", avgpairs(v_pairs))

(1, 2, 3, 4, 5, 6, 7, 8) => (1.5, 3.5, 5.5, 7.5)

In [27]:
DEFAULT_ALPHA = 1.25
def scale_alpha(v, alpha=DEFAULT_ALPHA):
return scale(v, alpha)
def latex_scale_alpha(x, alpha=DEFAULT_ALPHA):
return r'\mathrm{{scale}}_{{{}}}\left( {} \right)'.format(alpha, x)

display_math(r'''{} \equiv {} \cdot {}'''.format(latex_scale_alpha('x'), DEFAULT

In [28]:
# Recall: In the previous code cells,
# - `va`, `vb`, and `sigma` are given;
# - `va_scaled = scale(va, sigma)`
# - `ve = add(va_scaled, vb)`

# Let `u0 = f(ve)` where, again, `ve = sigma*va + vb`


u0 = scale_alpha(ve)

u0_latex = latex_vector(u0)
arg_str = r'{} {} + {}'.format(sigma, va_latex, vb_latex)
lhs_str = latex_scale_alpha(arg_str)
arg2_str = ve_latex
mid_str = latex_scale_alpha(arg2_str)
rhs_str = u0_latex
display_math(r'u_0 \equiv {} = {} = {}'.format(lhs_str, mid_str, rhs_str))

axes = subplots((1, 2))


new_blank_plot(axes[0], title='black = sigma*blue + red')
draw_vector2d(va_scaled, color='blue', alpha=0.5)
draw_vector2d(vb, origin=va_scaled, color='red', alpha=0.5)
draw_vector2d(ve)

new_blank_plot(axes[1], title='f(black)')
draw_vector2d(scale_alpha(ve))

In [29]:
display_math(r'''\frac{{{}}}{{{}}} = {}'''.format(latex_norm(rhs_str),
latex_norm(arg_str),
norm(u0) / norm(ve)))
In [30]:
# sigma*f(va) + f(vb)
u1_a = scale_alpha(va)
u1_aa = scale(u1_a, sigma)
u1_b = scale_alpha(vb)
u1 = add(u1_aa, u1_b)

display_math(r'''u_1 \equiv {} \cdot {} + {} = {}'''.format(sigma,


latex_scale_alpha(va
latex_scale_alpha(vb
latex_vector(u1)))

_, axes = plt.subplots(1, 4, figsize=(19, 4), sharey='row')

new_blank_plot(axes[0], title='blue, red')


draw_vector2d(va, color='blue')
draw_vector2d(vb, color='red')

new_blank_plot(axes[1], title='f(blue), f(red)')


draw_vector2d(u1_a, color='blue')
draw_vector2d(u1_b, color='red')

new_blank_plot(axes[2], title='sigma * f(blue)')


draw_vector2d(u1_aa, color='blue')

new_blank_plot(axes[3], title='black = sigma*f(blue) + f(red)')


draw_vector2d(u1_aa, color='blue', alpha=0.5)
draw_vector2d(u1_b, origin=u1_aa, color='red', alpha=0.5)
draw_vector2d(u1)
In [31]:
def random_angle(min_angle=0, max_angle=None):
"""
Returns a random angle in the specified interval
or (0, pi) if no interval is given.
"""
from math import pi
from random import uniform
if max_angle is None: max_angle = pi
return uniform(0, max_angle)

def rotate(v, theta=0):


from math import cos, sin
assert dim(v) == 2, "Input vector must be 2-D."
return vector(v[0]*cos(theta) - v[1]*sin(theta),
v[0]*sin(theta) + v[1]*cos(theta))

v_rand = normalize_vector(random_vector())
theta_rand = random_angle()
w_rand = rotate(v_rand, theta_rand)

figure()
new_blank_plot(xlim=(-1, 1), ylim=(-1, 1),
title=r'black = rotate blue by $\approx${:.0f}$^\circ$ counterclo
draw_vector2d(v_rand, color='blue', width=0.05)
draw_vector2d(w_rand, color='black', width=0.05)
matrix(x0, x1, ...)

In [32]:
def matrix(*cols):
if len(cols) > 2:
a_cols = cols[:-1]
b_cols = cols[1:]
for k, (a, b) in enumerate(zip(a_cols, b_cols)):
assert dim(a) == dim(b), \
"Columns {} and {} have different lengths ({} vs. {})".format
return tuple(cols)

def num_cols(X):
return len(X)

def num_rows(X):
return dim(X[0]) if num_cols(X) >= 1 else 0

# Demo
X = matrix(vector(3, 5), vector(-1, 2))
print("X =", X)

X = ((3, 5), (-1, 2))

In [33]:
def matelem(X, row, col):
assert col < dim(X), "Column {} is invalid (matrix only has {} columns).".fo
x_j = X[col]
assert row < dim(x_j), "Row {} is invalid (matrix only has {} rows).".format
return x_j[row]

def latex_matrix(X):
m, n = num_rows(X), num_cols(X)
s = r'\left[\begin{matrix}'
for i in range(m):
if i > 0: s += r' \\' # New row
for j in range(n):
if j > 0: s += ' & '
s += str(matelem(X, i, j))
s += r' \end{matrix}\right]'
return s

X_latex = latex_matrix(X)
display_math('X = ' + X_latex)

In [34]:
def matvec(X, v):
assert dim(X) == dim(v), "Matrix and vector have mismatching shapes."
w = [0] * num_rows(X)
for x_j, v_j in zip(X, v):
w = add(w, scale(x_j, v_j))
return w

v = vector(2, -4)
w = matvec(X, v)

v_latex = latex_vector(v)
w_latex = latex_vector(w)
display_math('X v = ' + X_latex + v_latex + ' = ' + w_latex)
In [35]:
XT = matrix(vector(0, 0.4, 0.6),
vector(0.5, 0.1, 0.4),
vector(0.3, 0.7, 0))
r = [vector(0, 1, 0)]
r.append(matvec(XT, r[-1]))

XT_latex = latex_matrix(XT)
r_latex = [latex_vector(ri) for ri in r]
display_math(r'X^T r(0) = {} {} = {} = r(1).'.format(XT_latex, r_latex[0], r_lat
In [36]:
r = [vector(0, 1, 0)] # Start over
for _ in range(10):
r.append(matvec(XT, r[-1]))

# Display the last few iterations:


k_left = min(3, len(r))
for k in range(len(r)-k_left, len(r)):
display_math(r'r({}) = {}'.format(k, latex_vector(r[k])))

In [37]:
# Here is the matrix from above:
Z = matrix(vector(3, 5), vector(-1, 2))

# Or, uncomment this statement to try a random matrix:


#Z = matrix(random_vector(v_min=-5, v_max=5), random_vector(v_min=-5, v_max=5))

# Or, uncomment this statement to try a random diagonal matrix


#Z = [[1.5, 0.0], [0.0, 3.0]]

display_math('Z = ' + latex_matrix(Z))

figure(figsize=(6, 6))
new_blank_plot(xlim=(-6, 6), ylim=(-6, 6))
for _ in range(50):
vk = normalize_vector(random_vector())
wk = matvec(Z, vk)
dk = sub(wk, vk)
draw_vector2d(dk, origin=vk, width=0.05)
matmat(A, B)
In [38]: def matmat(A, B):
m, k_A = num_rows(A), num_cols(A)
k_B, n = num_rows(B), num_cols(B)
assert k_A == k_B, "Inner-dimensions of `A` and `B` do not match."

C_cols = []
for bi in B:
C_cols.append(matvec(A, bi))
C = matrix(*C_cols)
return C

In [39]:
A = matrix(vector(1, 2, 3), vector(4, 5, 6), vector(7, 8, 9))
B = matrix(vector(-1, -1, -1), vector(1, 1, 1), vector(0.5, 0.25, 0.125))
C = matmat(A, B)

A_latex = latex_matrix(A)
B_latex = latex_matrix(B)
C_latex = latex_matrix(C)
display_math(r'{} \cdot {} = {}'.format(A_latex, B_latex, C_latex))

You might also like