Rheology studies how materials deform and flow under applied stress. Water has a single
viscosity. A polymer melt such as molten PBT does not. Its resistance to flow depends on shear
rate, temperature, and processing history. The goal here is to fix the two governing laws —
the Newtonian law and the power law — and to see, molecularly, why a melt thins as you
shear it harder.
Five scenes: simple shear, the Newtonian line $\tau=\eta\dot{\gamma}$, the power-law viscosity
drop with chain alignment, the temperature shift, and twin-screw-extruder intuition. The static
poster is the last frame;
constants are pedagogical, not a calibrated PBT grade.
Picture a fluid between two parallel plates. The bottom plate is fixed. The top plate is pulled
sideways. The layer touching the top moves fastest; the layer near the bottom barely moves. So
neighboring layers slide past one another. That sliding is shear.
Three quantities describe it. The shear stress $\tau$ is force per unit area
$[\mathrm{Pa}]$ — how hard we push to deform the material. The shear rate
$\dot{\gamma}$ is the velocity gradient $[\mathrm{s}^{-1}]$; for a planar profile $u(y)$,
$$ \dot{\gamma}=\frac{du}{dy}. $$
The viscosity $\eta$ is resistance to flow $[\mathrm{Pa\cdot s}]$, defined in simple
shear by $\eta=\tau/\dot{\gamma}$.
The simplest idealization is the Newtonian law: stress is proportional to shear rate.
$$ \tau=\eta\,\dot{\gamma},\qquad \eta=\text{const at fixed }T,\,p,\text{ composition.} $$
Double the shear rate and you double the stress. Water and simple solvents are approximately
Newtonian under ordinary conditions.
A non-Newtonian fluid does not obey $\tau=\eta\dot{\gamma}$ with constant $\eta$. Instead the
apparent viscosity depends on the flow itself, $\eta_{\mathrm{app}}=\eta_{\mathrm{app}}(\dot{\gamma})$.
Polymer melts are usually non-Newtonian, and the reason is molecular. A melt is a tangle of long
chains. At low shear the chains are entangled and randomly oriented. At higher shear the flow
orients and stretches them, so they slide past each other more easily. The material offers less
resistance. This is shear thinning:
$$ \frac{d\eta_{\mathrm{app}}}{d\dot{\gamma}}<0. $$
The standard first model is the Ostwald–de Waele power law. Dividing by $\dot{\gamma}$ turns
the stress law into a viscosity law.
$$ \tau=K\dot{\gamma}^{\,n},\qquad
\eta_{\mathrm{app}}(\dot{\gamma})=\frac{\tau}{\dot{\gamma}}=K\dot{\gamma}^{\,n-1}. $$
Here $K$ is the consistency index and $n$ the flow-behavior index. The sign of $n-1$ decides
everything.
If $n=1$, then $\eta_{\mathrm{app}}=K$ is constant: Newtonian. If $0<n<1$, then $n-1<0$, so
$\eta_{\mathrm{app}}$ falls as $\dot{\gamma}$ rises: shear thinning. If $n>1$, viscosity rises
with shear rate: shear thickening.
A melt typically has $0<n<1$. Take $n=\tfrac12$, so $\eta_{\mathrm{app}}=K\dot{\gamma}^{-1/2}$.
Then raising the shear rate by $100$ drops the apparent viscosity by $10$. That is a power-law
drop, not an exponential one.
Viscosity also falls with temperature, because molecular motion becomes easier. A common local
model is Arrhenius, $\eta(T)=A\exp(E_a/RT)$, with $T$ in absolute Kelvin. Because $T$ sits in the
denominator, raising $T$ shrinks the exponent and lowers $\eta$.
Combining the two effects, and normalizing the Arrhenius factor about a reference temperature
$T_{\mathrm{ref}}$, gives the model used in the animation:
$$ \eta_{\mathrm{app}}(\dot{\gamma},T)
=K_{\mathrm{ref}}\,
\exp\!\left[\frac{E_a}{R}\left(\frac{1}{T}-\frac{1}{T_{\mathrm{ref}}}\right)\right]
\dot{\gamma}^{\,n-1}. $$
Two leading trends: higher shear rate lowers viscosity when $n<1$, and higher temperature lowers
viscosity. This is a pedagogical form, not a calibrated fit to any real PBT grade.
Molten PBT is not water. Its viscosity is not a single number but a process-dependent quantity
$\eta_{\mathrm{app}}(\dot{\gamma},T)$. In a twin-screw extruder the screws create complicated
local flow fields. The local shear rate depends on screw speed, screw geometry, channel depth,
gap size, fill level, and pressure.
The response is a coupled chain:
$$ \text{screw speed}\;\to\;\dot{\gamma}\;\to\;\eta_{\mathrm{app}}\;\to\;
\text{pressure, torque, mixing, residence time, heat.} $$
For a Newtonian fluid, changing $\dot{\gamma}$ changes the stress but not the viscosity. For a
shear-thinning melt, it changes both. That is why an extruder is a coupled thermomechanical
system, not merely a liquid pump.
Newtonian: $\tau=\eta\dot{\gamma}$ with constant $\eta$. Polymer melt: $\tau=K\dot{\gamma}^{\,n}$,
$\eta_{\mathrm{app}}=K\dot{\gamma}^{\,n-1}$, $0<n<1$. The viscosity of molten PBT is an apparent,
process-dependent quantity — specify it together with shear rate, temperature, and grade.
Python source for the animation (numpy + matplotlib + pillow; ffmpeg optional)
"""
Rheology intro animation: Newtonian fluid vs. shear-thinning polymer melt (PBT-like).
Companion to entries/mathematics/rheology-pbt-extrusion.html.
Run: python3 create_rheology_gif.py (writes GIF / PNG / optional MP4 alongside this file)
------------------------------------------------------------------------------
The physics this animation teaches
------------------------------------------------------------------------------
Rheology studies how materials deform and flow under applied stress. In simple
shear a material is sheared between two plates; we track three quantities:
tau shear stress [Pa] force per unit area
gdot shear rate [1/s] du/dy, the velocity gradient
eta viscosity [Pa s] resistance to flow, eta = tau / gdot
Newtonian fluid (water, simple solvents at fixed T):
tau = eta * gdot, eta = constant.
So tau grows LINEARLY with gdot and the ratio tau/gdot never changes.
Shear-thinning polymer melt (Ostwald-de Waele power law):
tau = K * gdot**n, eta_app(gdot) = tau / gdot = K * gdot**(n-1).
For 0 < n < 1 the exponent n-1 < 0, so apparent viscosity DECREASES as the
shear rate increases. Molecularly: long entangled chains orient with the flow
at high shear, offering less resistance. This is a POWER-LAW drop, not an
exponential one.
Temperature: viscosity also drops with temperature. We use a normalized
Arrhenius-type factor about a reference temperature T_ref,
eta_app(gdot, T) = K_ref * exp[ (Ea/R) * (1/T - 1/T_ref) ] * gdot**(n-1).
This is a PEDAGOGICAL model. The constants below are illustrative, not a
calibrated fit to any real PBT grade.
"""
import numpy as np
import matplotlib
matplotlib.use("Agg") # headless: render frames without a display
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, Rectangle, Circle, FancyBboxPatch
from matplotlib.animation import FuncAnimation, PillowWriter
try:
from matplotlib.animation import FFMpegWriter
except Exception: # pragma: no cover - extremely rare
FFMpegWriter = None
from pathlib import Path
# --------------------------------------------------------------------------
# Pedagogical material parameters (NOT a calibrated PBT fit)
# --------------------------------------------------------------------------
K_ref = 1000.0 # Pa s^n, consistency index at T_ref
n = 0.45 # shear-thinning index, 0 < n < 1
eta_newtonian = 300.0 # Pa s, constant Newtonian viscosity
Ea = 50_000.0 # J/mol, apparent activation energy for flow
R = 8.314 # J/(mol K), gas constant
T_ref = 533.15 # K (~260 C), reference temperature
T_cold = 513.15 # K (~240 C)
T_hot = 553.15 # K (~280 C)
# Shear-rate axis: log-spaced over [1e-1, 1e3] s^-1
GDOT = np.logspace(-1, 3, 400)
# --------------------------------------------------------------------------
# Rheological model functions
# --------------------------------------------------------------------------
def eta_polymer(gdot, T):
"""Apparent viscosity of the polymer melt, eta_app(gdot, T) [Pa s].
Power-law shear thinning times a normalized Arrhenius temperature factor.
"""
arrhenius = np.exp(Ea / R * (1.0 / T - 1.0 / T_ref))
return K_ref * arrhenius * gdot ** (n - 1.0)
def tau_polymer(gdot, T):
"""Shear stress of the polymer melt, tau = eta_app * gdot [Pa]."""
return eta_polymer(gdot, T) * gdot
def tau_newtonian(gdot):
"""Newtonian shear stress, tau = eta * gdot [Pa]."""
return eta_newtonian * gdot
# --------------------------------------------------------------------------
# Color palette (<= 4 main colors) and global style
# --------------------------------------------------------------------------
C_NEWT = "#1f6fb2" # Newtonian: blue
C_POLY = "#e8820c" # polymer melt (reference T): orange
C_COLD = "#5e3c99" # cold curve: purple
C_HOT = "#d7301f" # hot curve: red
C_INK = "#222222" # text / structure
C_FAINT = "#888888"
plt.rcParams.update({
"font.size": 15,
"font.family": "DejaVu Sans",
"axes.titlesize": 17,
"axes.labelsize": 16,
"axes.edgecolor": "#444444",
"xtick.labelsize": 12,
"ytick.labelsize": 12,
"mathtext.fontset": "cm",
"figure.facecolor": "white",
"axes.facecolor": "white",
"savefig.facecolor": "white",
})
# --------------------------------------------------------------------------
# Frame / scene bookkeeping
# --------------------------------------------------------------------------
fps = 12
duration_sec = 16
n_frames = fps * duration_sec # 192 frames
scene_edges = [0, 36, 72, 120, 156, 192] # scene boundaries in frames
def scene_progress(frame, start, end):
"""Fractional progress in [0,1] of `frame` within scene [start, end)."""
return np.clip((frame - start) / (end - start), 0.0, 1.0)
def smoothstep(x):
"""Smooth 0->1 ease (zero slope at both ends)."""
x = np.clip(x, 0.0, 1.0)
return x * x * (3.0 - 2.0 * x)
# --------------------------------------------------------------------------
# Figure scaffold: a single 1280x720 canvas reused for every frame.
# We use one full-canvas axes for drawings/text and add plot axes per scene.
# --------------------------------------------------------------------------
DPI = 100
fig = plt.figure(figsize=(12.8, 7.2), dpi=DPI)
# Background axes covering the whole figure for titles, schematics, labels.
bg = fig.add_axes([0, 0, 1, 1])
bg.set_xlim(0, 1)
bg.set_ylim(0, 1)
bg.axis("off")
def _text(x, y, s, size=16, color=C_INK, ha="left", va="center",
weight="normal", style="normal", alpha=1.0):
"""Convenience wrapper for background-axes text in figure coordinates."""
return bg.text(x, y, s, size=size, color=color, ha=ha, va=va,
weight=weight, style=style, alpha=alpha, zorder=10)
# --------------------------------------------------------------------------
# Helper: simple-shear cell (two plates + sliding fluid layers)
# --------------------------------------------------------------------------
def draw_shear_cell(ax, progress, mode="newtonian"):
"""Draw a sheared fluid between a fixed bottom plate and a moving top plate.
progress in [0,1] animates the top plate sliding right and the per-layer
velocity arrows growing (longer near the top: a linear velocity gradient).
`mode` only sets the accent color of the fluid block.
"""
ax.clear()
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
accent = C_NEWT if mode == "newtonian" else C_POLY
x0, x1 = 1.2, 8.0
y_bot, y_top = 2.0, 8.0
shift = 1.6 * progress # how far the top plate has slid
# Fluid block (light fill) -- the sheared material.
ax.add_patch(Rectangle((x0, y_bot), x1 - x0, y_top - y_bot,
facecolor=accent, alpha=0.10,
edgecolor="none", zorder=1))
# Fixed bottom plate (hatched bar) and moving top plate.
ax.add_patch(Rectangle((x0 - 0.3, y_bot - 0.45), x1 - x0 + 0.6, 0.45,
facecolor="#cfcfcf", edgecolor=C_INK, zorder=3))
ax.add_patch(Rectangle((x0 - 0.3 + shift, y_top), x1 - x0 + 0.6, 0.45,
facecolor="#cfcfcf", edgecolor=C_INK, zorder=3))
# Fluid layers: horizontal lines that skew with height (top moves most).
n_layers = 7
for i in range(n_layers):
frac = i / (n_layers - 1) # 0 at bottom, 1 at top
y = y_bot + frac * (y_top - y_bot)
dx = shift * frac # linear velocity profile
ax.plot([x0 + dx, x1 + dx], [y, y], color=accent, lw=1.4,
alpha=0.55, zorder=2)
# Velocity arrow: length grows with height -> shear rate = du/dy.
arrow_len = 0.25 + 2.4 * frac * (0.4 + 0.6 * progress)
if arrow_len > 0.05:
ax.add_patch(FancyArrowPatch(
(x1 + dx + 0.15, y), (x1 + dx + 0.15 + arrow_len, y),
arrowstyle="-|>", mutation_scale=12,
color=accent, lw=2.0, zorder=4))
# Annotate the moving / fixed plates.
ax.text((x0 + x1) / 2 + shift, y_top + 0.7, "top plate moves →",
ha="center", va="bottom", fontsize=12.5, color=C_INK)
ax.text((x0 + x1) / 2, y_bot - 0.95, "fixed bottom plate",
ha="center", va="top", fontsize=12.5, color=C_INK)
# Velocity-gradient bracket label.
ax.text(x1 + 1.9, (y_bot + y_top) / 2,
r"$u(y)$" "\n" r"$\dot{\gamma}=du/dy$",
ha="left", va="center", fontsize=13, color=accent)
# --------------------------------------------------------------------------
# Helper: cartoon polymer chains, oriented by `alignment` in [0,1]
# --------------------------------------------------------------------------
def draw_polymer_chains(ax, alignment, seed=0):
"""Draw squiggly polymer chains; alignment 0 = tangled, 1 = flow-aligned.
Each chain is a sine squiggle with a random phase. At low alignment the
chains take random orientations; at high alignment they rotate toward the
horizontal (flow) direction and stretch out, the molecular picture behind
shear thinning.
"""
ax.clear()
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
a = smoothstep(alignment)
rng = np.random.default_rng(seed) # fixed seed -> stable chains across frames
n_chains = 9
t = np.linspace(-1.0, 1.0, 60)
for _ in range(n_chains):
cx = rng.uniform(1.8, 8.2)
cy = rng.uniform(2.0, 8.0)
length = (1.1 + 0.4 * rng.random()) * (1.0 + 0.9 * a) # stretch out
amp = (0.45 + 0.35 * rng.random()) * (1.0 - 0.75 * a) # uncrinkle
phase = rng.uniform(0, 2 * np.pi)
waves = rng.uniform(2.0, 3.5)
# Local chain shape: a stretched axis (xs) + transverse squiggle (ys).
xs = length * t
ys = amp * np.sin(waves * np.pi * t + phase)
# Orientation: random when tangled, -> horizontal as it aligns.
theta0 = rng.uniform(0, 2 * np.pi)
theta = (1.0 - a) * theta0 # collapse toward 0 rad (flow direction)
ct, st = np.cos(theta), np.sin(theta)
X = cx + ct * xs - st * ys
Y = cy + st * xs + ct * ys
ax.plot(X, Y, color=C_POLY, lw=2.2, alpha=0.85, solid_capstyle="round")
# Flow-direction velocity arrows, growing with alignment (= shear rate).
for yk in (3.0, 5.0, 7.0):
L = 0.6 + 2.4 * a
ax.add_patch(FancyArrowPatch((0.6, yk), (0.6 + L, yk),
arrowstyle="-|>", mutation_scale=12,
color=C_FAINT, lw=1.8))
# --------------------------------------------------------------------------
# Helper: simplified twin-screw extruder schematic
# --------------------------------------------------------------------------
def draw_extruder(ax, progress):
"""Two intermeshing rotating screws in a barrel; melt moves left to right."""
ax.clear()
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
bx0, bx1, by0, by1 = 0.6, 9.4, 3.2, 6.8
# Barrel outline.
ax.add_patch(FancyBboxPatch((bx0, by0), bx1 - bx0, by1 - by0,
boxstyle="round,pad=0.02,rounding_size=0.25",
facecolor="#f1f1ec", edgecolor=C_INK, lw=1.8,
zorder=1))
# Two screw shafts drawn as a series of flights (slanted ticks) that
# appear to rotate/convey as `progress` advances.
phase = 2 * np.pi * progress
for cy in (by0 + 1.0, by1 - 1.0):
ax.plot([bx0 + 0.4, bx1 - 0.4], [cy, cy], color=C_FAINT, lw=1.2,
zorder=2)
nf = 16
for k in range(nf):
xf = bx0 + 0.6 + (bx1 - bx0 - 1.2) * k / (nf - 1)
s = 0.5 * np.sin(phase + k * 0.9) # flight tilt animates
ax.plot([xf - 0.18, xf + 0.18], [cy - 0.45 + s * 0.25,
cy + 0.45 + s * 0.25],
color=C_POLY, lw=2.4, solid_capstyle="round", zorder=3)
# Melt moving left -> right: a few markers advancing with progress.
for j in range(5):
mx = bx0 + 0.8 + ((j / 5.0 + progress) % 1.0) * (bx1 - bx0 - 1.6)
ax.add_patch(Circle((mx, (by0 + by1) / 2), 0.16,
facecolor=C_POLY, edgecolor="none",
alpha=0.7, zorder=4))
ax.text((bx0 + bx1) / 2, by1 + 0.5,
"twin-screw extruder: two intermeshing screws convey the melt →",
ha="center", va="bottom", fontsize=12.5, color=C_INK)
# --------------------------------------------------------------------------
# Helper: clean up a Matplotlib plot axis (spines, grid)
# --------------------------------------------------------------------------
def setup_clean_axis(ax):
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.grid(True, which="both", color="#dddddd", lw=0.6, alpha=0.7)
ax.set_axisbelow(True)
def save_last_frame(fig, filename):
"""Save the current figure state as a PNG poster of the final frame."""
fig.savefig(filename, dpi=DPI)
# --------------------------------------------------------------------------
# Per-scene renderers. Each receives local progress p in [0,1].
# We create/destroy plot axes per frame so the single canvas can morph.
# --------------------------------------------------------------------------
# Track dynamically-added axes so we can clear them each frame.
_dyn_axes = []
def _clear_dynamic():
for ax in _dyn_axes:
ax.remove()
_dyn_axes.clear()
bg.clear()
bg.set_xlim(0, 1)
bg.set_ylim(0, 1)
bg.axis("off")
def _header(title, subtitle=None):
_text(0.5, 0.95, title, size=22, ha="center", weight="bold")
if subtitle:
_text(0.5, 0.895, subtitle, size=14, ha="center", color=C_FAINT)
def scene1(p):
"""What is shear? Animated plates + growing velocity arrows."""
_header("1. What is shear?")
ax = fig.add_axes([0.05, 0.08, 0.62, 0.74]); _dyn_axes.append(ax)
draw_shear_cell(ax, smoothstep(p), mode="newtonian")
_text(0.70, 0.70,
"Shear = neighboring layers\nsliding past one another",
size=15)
_text(0.70, 0.50,
r"Shear rate $\dot{\gamma}$: speed gradient",
size=15, color=C_NEWT)
_text(0.70, 0.38,
r"Shear stress $\tau$: force per area",
size=15, color=C_NEWT)
_text(0.70, 0.22, r"$\eta = \tau/\dot{\gamma}$ (viscosity)",
size=16)
def scene2(p):
"""Newtonian fluid: tau = eta * gdot, linear, marker sweeps the line."""
_header("2. Newtonian fluid", r"$\tau = \eta\,\dot{\gamma}$, $\eta$ constant")
# Left: shear cell.
axL = fig.add_axes([0.03, 0.08, 0.40, 0.70]); _dyn_axes.append(axL)
draw_shear_cell(axL, 1.0, mode="newtonian")
# Right: tau vs gdot on LINEAR axes.
axR = fig.add_axes([0.55, 0.16, 0.40, 0.64]); _dyn_axes.append(axR)
setup_clean_axis(axR)
g = np.linspace(0, 1000, 200)
axR.plot(g, tau_newtonian(g), color=C_NEWT, lw=3)
axR.set_xlabel(r"$\dot{\gamma}\ \mathrm{(s^{-1})}$")
axR.set_ylabel(r"$\tau\ \mathrm{(Pa)}$")
axR.set_xlim(0, 1000)
axR.set_ylim(0, tau_newtonian(1000) * 1.05)
# Marker sweeping along the line as gdot increases.
gm = 1000 * smoothstep(p)
axR.plot([gm], [tau_newtonian(gm)], "o", color=C_NEWT, ms=11,
markeredgecolor="white")
axR.text(0.05, 0.90,
r"$\eta=\tau/\dot{\gamma}=300\ \mathrm{Pa\,s}$ (fixed)",
transform=axR.transAxes, fontsize=13, color=C_NEWT)
_text(0.5, 0.045,
"Double the shear rate, double the stress. The ratio stays fixed.",
size=15, ha="center")
def scene3(p):
"""Polymer melt: chains align (left) + log-log eta drop (right)."""
_header("3. Polymer melt: chains align, viscosity drops")
a = smoothstep(p) # alignment / marker position driver
# Left: polymer chains becoming aligned.
axL = fig.add_axes([0.03, 0.10, 0.40, 0.66]); _dyn_axes.append(axL)
draw_polymer_chains(axL, a, seed=3)
if a < 0.5:
_text(0.23, 0.085,
"low shear: chains tangled;\nhigh apparent viscosity",
size=13, ha="center", color=C_POLY)
else:
_text(0.23, 0.085,
"higher shear: chains align;\napparent viscosity drops",
size=13, ha="center", color=C_POLY)
# Right: eta_app vs gdot on LOG-LOG axes.
axR = fig.add_axes([0.55, 0.17, 0.40, 0.60]); _dyn_axes.append(axR)
setup_clean_axis(axR)
axR.set_xscale("log"); axR.set_yscale("log")
axR.plot(GDOT, eta_polymer(GDOT, T_ref), color=C_POLY, lw=3,
label="polymer melt")
axR.plot(GDOT, np.full_like(GDOT, eta_newtonian), color=C_NEWT, lw=2.5,
ls="--", label="Newtonian")
axR.set_xlabel(r"$\dot{\gamma}\ \mathrm{(s^{-1})}$")
axR.set_ylabel(r"$\eta_{\mathrm{app}}\ \mathrm{(Pa\cdot s)}$")
axR.set_ylim(10, 1e4)
# Direct labels (no tiny legend).
axR.text(2e2, eta_newtonian * 1.25, "Newtonian $\\eta=300$",
color=C_NEWT, fontsize=12, ha="center")
axR.text(2e0, eta_polymer(2e0, T_ref) * 1.5,
r"$\eta_{\mathrm{app}}=K\dot{\gamma}^{\,n-1}$",
color=C_POLY, fontsize=13)
# Marker sweeping from low to high gdot along the polymer curve.
gm = 10 ** (-1 + 4 * a)
axR.plot([gm], [eta_polymer(gm, T_ref)], "o", color=C_POLY, ms=11,
markeredgecolor="white", zorder=5)
_text(0.5, 0.05,
r"$0<n<1$: power-law drop, not exponential drop.",
size=15, ha="center")
def scene4(p):
"""Temperature effect: cold / ref / hot curves; hot animates in lower."""
_header("4. Temperature lowers viscosity")
ax = fig.add_axes([0.30, 0.16, 0.45, 0.60]); _dyn_axes.append(ax)
setup_clean_axis(ax)
ax.set_xscale("log"); ax.set_yscale("log")
ax.set_xlabel(r"$\dot{\gamma}\ \mathrm{(s^{-1})}$")
ax.set_ylabel(r"$\eta_{\mathrm{app}}\ \mathrm{(Pa\cdot s)}$")
ax.set_ylim(10, 3e4)
a = smoothstep(p)
# Cold and reference are drawn immediately; the hot curve fades/drops in.
ax.plot(GDOT, eta_polymer(GDOT, T_cold), color=C_COLD, lw=3)
ax.plot(GDOT, eta_polymer(GDOT, T_ref), color=C_POLY, lw=3)
ax.plot(GDOT, eta_polymer(GDOT, T_hot), color=C_HOT, lw=3, alpha=a)
# Direct curve labels.
ax.text(1.2e-1, eta_polymer(1.2e-1, T_cold) * 1.05,
r"cold $240^\circ$C", color=C_COLD, fontsize=12.5)
ax.text(1.2e-1, eta_polymer(1.2e-1, T_ref) * 1.05,
r"ref $260^\circ$C", color=C_POLY, fontsize=12.5)
ax.text(1.2e-1, eta_polymer(1.2e-1, T_hot) * 0.78,
r"hot $280^\circ$C", color=C_HOT, fontsize=12.5, alpha=a)
_text(0.5, 0.86,
r"$\eta_{\mathrm{app}}(\dot{\gamma},T)=K_{\mathrm{ref}}\,"
r"\exp\!\left[\frac{E_a}{R}\left(\frac{1}{T}-\frac{1}{T_{\mathrm{ref}}}"
r"\right)\right]\dot{\gamma}^{\,n-1}$",
size=15, ha="center")
_text(0.5, 0.06,
"Higher temperature usually lowers melt viscosity.",
size=15, ha="center")
def scene5(p):
"""Twin-screw extruder intuition + causal chain + final takeaway."""
_header("5. Twin-screw extruder intuition")
ax = fig.add_axes([0.30, 0.30, 0.66, 0.46]); _dyn_axes.append(ax)
draw_extruder(ax, p)
# Causal chain on the left.
chain = [
"screw speed ↑",
"local shear rate ↑",
"polymer chains align",
"apparent viscosity ↓",
"torque / pressure / mixing change",
]
y = 0.74
for i, line in enumerate(chain):
shown = smoothstep(scene_progress(p, i * 0.12, i * 0.12 + 0.25))
_text(0.04, y, line, size=13.5, color=C_INK, alpha=shown)
if i < len(chain) - 1:
_text(0.07, y - 0.045, "↓", size=13, color=C_FAINT,
alpha=shown)
y -= 0.095
# Caution box.
bg.add_patch(FancyBboxPatch((0.31, 0.165), 0.40, 0.085,
boxstyle="round,pad=0.01,rounding_size=0.02",
transform=bg.transAxes,
facecolor="#fff4e6", edgecolor=C_POLY,
lw=1.4, zorder=9))
_text(0.51, 0.207,
r"RPM is a machine setting. $\dot{\gamma}$ is a local flow variable.",
size=13.5, ha="center", color="#9a5a00")
# Final takeaway.
_text(0.5, 0.06,
"Molten PBT viscosity is not one fixed number: it depends on\n"
"shear rate, temperature, and processing history.",
size=15, ha="center", weight="bold")
# --------------------------------------------------------------------------
# Master frame dispatcher
# --------------------------------------------------------------------------
def render_frame(frame):
_clear_dynamic()
if frame < scene_edges[1]:
scene1(scene_progress(frame, scene_edges[0], scene_edges[1]))
elif frame < scene_edges[2]:
scene2(scene_progress(frame, scene_edges[1], scene_edges[2]))
elif frame < scene_edges[3]:
scene3(scene_progress(frame, scene_edges[2], scene_edges[3]))
elif frame < scene_edges[4]:
scene4(scene_progress(frame, scene_edges[3], scene_edges[4]))
else:
scene5(scene_progress(frame, scene_edges[4], scene_edges[5]))
return []
def main():
here = Path(__file__).parent
gif_path = here / "rheology_pbt_intro.gif"
mp4_path = here / "rheology_pbt_intro.mp4"
png_path = here / "rheology_pbt_intro_last_frame.png"
anim = FuncAnimation(fig, render_frame, frames=n_frames, blit=False)
# --- GIF (always) ---
anim.save(str(gif_path), writer=PillowWriter(fps=fps))
print("Created rheology_pbt_intro.gif")
# --- MP4 (only if ffmpeg is available) ---
mp4_ok = False
if FFMpegWriter is not None and FFMpegWriter.isAvailable():
try:
anim.save(str(mp4_path), writer=FFMpegWriter(fps=fps, bitrate=2400))
mp4_ok = True
print("Created rheology_pbt_intro.mp4")
except Exception as exc: # pragma: no cover
print(f"MP4 skipped because ffmpeg failed: {exc}")
if not mp4_ok and not mp4_path.exists():
print("MP4 skipped because ffmpeg is unavailable.")
# --- Last frame as PNG poster ---
render_frame(n_frames - 1)
save_last_frame(fig, str(png_path))
print("Created rheology_pbt_intro_last_frame.png")
if __name__ == "__main__":
main()
The Ostwald–de Waele power law $\tau=K\dot{\gamma}^{n}$ (hence
$\eta_{\mathrm{app}}=K\dot{\gamma}^{n-1}$) and the Arrhenius temperature factor are standard
introductory rheology. The constants here ($K_{\mathrm{ref}}$, $n=0.45$, $E_a$,
$T_{\mathrm{ref}}\approx260^\circ$C) are illustrative values chosen for a clean visualization, not
a material fit to a specific PBT grade. Companion code:
code/rheology_pbt/create_rheology_gif.py.