codexviscosity-strain-versus-rotation.py
viscosity-strain-versus-rotation.py
"""Compare exact material-patch extension and rotation; output to the current directory.
Tested with Python 3.14.4, matplotlib 3.10.7 and numpy 2.3.5.
Build from the repository root: make _media/viscosity-strain-versus-rotation.png
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
import numpy as np
a = omega = 1.0 # s^-1
mu = 0.10 # Pa s
time = 0.65 # s
extension_gradient = np.diag([a, -a])
rotation_gradient = np.array([[0.0, -omega], [omega, 0.0]])
extension_D = 0.5 * (extension_gradient + extension_gradient.T)
rotation_D = 0.5 * (rotation_gradient + rotation_gradient.T)
extension_map = np.diag([np.exp(a * time), np.exp(-a * time)])
angle = omega * time
rotation_map = np.array([[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
square = 0.55 * np.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]])
assert np.allclose(rotation_D, 0.0)
assert np.allclose(2 * mu * extension_D, np.diag([0.20, -0.20]))
assert np.isclose(np.linalg.norm(extension_gradient), np.linalg.norm(rotation_gradient))
assert np.isclose(np.linalg.det(extension_map), 1.0)
assert np.isclose(np.linalg.det(rotation_map), 1.0)
assert np.allclose(rotation_map.T @ rotation_map, np.eye(2))
rotated = square @ rotation_map.T
assert np.allclose(np.linalg.norm(np.diff(rotated, axis=0), axis=1), 1.10)
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
blue = "#1766a0"
orange = "#b04d13"
fig, axes = plt.subplots(1, 2, figsize=(10, 6), dpi=120)
fig.subplots_adjust(left=0.08, right=0.96, top=0.78, bottom=0.33, wspace=0.32)
fig.suptitle("Viscous stress responds to strain, not rigid rotation", y=0.97, fontsize=17)
fig.text(0.5, 0.90, r"$a=\Omega=1\ \mathrm{s^{-1}},\quad \mu=0.10\ \mathrm{Pa\,s},\quad t=0.65\ \mathrm{s}$", ha="center", fontsize=14)
x, y = np.meshgrid(np.linspace(-1.20, 1.20, 7), np.linspace(-1.20, 1.20, 7))
for ax, gradient, mapping, title in zip(
axes, [extension_gradient, rotation_gradient], [extension_map, rotation_map],
[r"Planar extension: $\mathbf{u}=(ax,-ay,0)$", r"Rigid rotation: $\mathbf{u}=(-\Omega y,\Omega x,0)$"]):
ax.set_title(title, fontsize=13, pad=12)
ax.set_aspect("equal")
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1.5, 1.5)
ax.set_xticks([-1, 0, 1])
ax.set_yticks([-1, 0, 1])
ax.set_xlabel("x (m)", labelpad=3)
ax.set_ylabel("y (m)", labelpad=3)
ax.spines[["top", "right"]].set_visible(False)
ax.axhline(0, color="#e0e5e9", lw=0.8, zorder=0)
ax.axvline(0, color="#e0e5e9", lw=0.8, zorder=0)
vx = gradient[0, 0] * x + gradient[0, 1] * y
vy = gradient[1, 0] * x + gradient[1, 1] * y
ax.quiver(x, y, vx, vy, color=blue, alpha=0.65, angles="xy", scale_units="xy", scale=5,
width=0.007, headwidth=4, zorder=1)
transformed = square @ mapping.T
ax.plot(square[:, 0], square[:, 1], "--", lw=2, color="#67737f", zorder=3)
ax.plot(transformed[:, 0], transformed[:, 1], color=orange, lw=2.8, zorder=4)
ax.plot(*square[2], "o", color="#67737f", ms=6, zorder=5)
ax.plot(*transformed[2], "o", color=orange, ms=7, zorder=5)
fig.text(0.29, 0.19, r"$D=\mathrm{diag}(1,-1,0)\ \mathrm{s^{-1}}$", ha="center", fontsize=13)
fig.text(0.29, 0.14, r"$\sigma_{\mathrm{visc}}=\mathrm{diag}(0.20,-0.20,0)\ \mathrm{Pa}$", ha="center", fontsize=12)
fig.text(0.73, 0.19, r"$\nabla\mathbf{u}\ne0,\quad D=0$", ha="center", fontsize=14)
fig.text(0.73, 0.14, r"$\sigma_{\mathrm{visc}}=0$", ha="center", fontsize=14)
handles = [Line2D([0], [0], color=blue, lw=2, label="instantaneous velocity"),
Line2D([0], [0], color="#67737f", lw=2, ls="--", label="patch at t = 0"),
Line2D([0], [0], color=orange, lw=2.8, label="patch at t = 0.65 s")]
fig.legend(handles=handles, loc="lower center", bbox_to_anchor=(0.5, 0.055), ncol=3, frameon=False, fontsize=11)
fig.text(0.5, 0.025, "Both preserve area. Extension changes shape; rotation preserves all distances.", ha="center", fontsize=12)
output = Path.cwd() / f"{Path(__file__).stem}.png"
fig.savefig(output, facecolor="white", transparent=False)
plt.close(fig)
print(f"{output.name}: 1200 x 720; |grad u|_F={np.linalg.norm(extension_gradient):.8f} s^-1 for both")
print(f"det(extension map)={np.linalg.det(extension_map):.8f}; det(rotation map)={np.linalg.det(rotation_map):.8f}")
print(f"extension stress diagonal={(2 * mu * extension_D).diagonal().tolist()} Pa; rotation D={rotation_D.tolist()}")