Critical teaching point: Fluid leaving the ascending loop and early distal tubule is always dilute (~100 mOsm/L), regardless of ADH levels. ADH has no effect here. - Guyton and Hall, p. 376
| Segment | ADH Present | No ADH |
|---|---|---|
| Proximal tubule | 300 mOsm/L | 300 mOsm/L |
| Descending loop | rises to 1200 | rises to 1200 |
| Ascending loop | falls to 100 | falls to 100 |
| Late distal/collecting | rises (water reabsorbed) | stays low/falls further |
| Final urine | ~1200 mOsm/L, 0.2 mL/min | ~50 mOsm/L, 20 mL/min |

Can you make short animations for each of those segments please
mkdir -p /tmp/workspace/nephron-animations && ls /tmp/workspace/nephron-animations
~/nephron-animations/make_animations.py
"""
Creates 6 animated GIFs explaining tubular fluid osmolarity changes
across each nephron segment (Guyton Figure 29-8).
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
from matplotlib.collections import LineCollection
from PIL import Image
import io
import os
OUT = "/tmp/workspace/nephron-animations"
# ── helpers ────────────────────────────────────────────────────────────────
def fig_to_pil(fig):
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=110, bbox_inches='tight',
facecolor=fig.get_facecolor())
buf.seek(0)
img = Image.open(buf).convert('RGBA')
return img.copy()
def save_gif(frames, path, duration=80, loop=0):
frames[0].save(path, save_all=True, append_images=frames[1:],
duration=duration, loop=loop, optimize=False)
def osm_color(osm, lo=50, hi=1200):
t = np.clip((osm - lo) / (hi - lo), 0, 1)
# blue (dilute) → yellow → red (concentrated)
r = t
g = 0.6 * (1 - abs(2*t - 1))
b = 1 - t
return (r, g, b)
def molecule_dots(ax, n, x_range, y_range, color, size=35, alpha=0.85):
xs = np.random.uniform(*x_range, n)
ys = np.random.uniform(*y_range, n)
ax.scatter(xs, ys, s=size, color=color, alpha=alpha, zorder=5)
# ══════════════════════════════════════════════════════════════════════════
# SEGMENT 1 – Proximal Tubule
# ══════════════════════════════════════════════════════════════════════════
def seg1_proximal():
frames = []
n_frames = 36
np.random.seed(42)
for f in range(n_frames):
fig, ax = plt.subplots(figsize=(7, 4.5))
fig.patch.set_facecolor('#0d1b2a')
ax.set_facecolor('#0d1b2a')
ax.set_xlim(0, 10); ax.set_ylim(0, 6)
ax.axis('off')
# Title
ax.text(5, 5.6, "Proximal Tubule", ha='center', va='top',
fontsize=14, fontweight='bold', color='white')
ax.text(5, 5.1, "~65% of water & solutes reabsorbed — osmolarity stays 300 mOsm/L",
ha='center', va='top', fontsize=8, color='#aad4f5')
# Tubule wall (wide horizontal tube)
tube_y_lo, tube_y_hi = 1.5, 3.5
tube_color = osm_color(300)
tube_bg = mpatches.FancyBboxPatch((0.5, tube_y_lo), 9, tube_y_hi - tube_y_lo,
boxstyle="round,pad=0.05",
linewidth=2, edgecolor='#aaaaaa',
facecolor=(*tube_color, 0.25))
ax.add_patch(tube_bg)
# Osmolarity label inside tube
ax.text(5, 2.5, "300 mOsm/L", ha='center', va='center',
fontsize=12, fontweight='bold', color='white', zorder=10)
# Moving solute dots (NaCl, glucose, AA) inside tube
phase = (f / n_frames)
n_solute = 38
rng = np.random.RandomState(f * 7 + 1)
xs = (rng.uniform(0.6, 9.4, n_solute) - phase * 2) % 8.8 + 0.6
ys = rng.uniform(tube_y_lo + 0.15, tube_y_hi - 0.15, n_solute)
for x, y in zip(xs[:20], ys[:20]):
ax.scatter(x, y, s=28, color='#f5c518', alpha=0.8, zorder=5) # NaCl
for x, y in zip(xs[20:30], ys[20:30]):
ax.scatter(x, y, s=22, color='#88e0ef', alpha=0.8, zorder=5) # water
for x, y in zip(xs[30:], ys[30:]):
ax.scatter(x, y, s=20, color='#c3f084', alpha=0.8, zorder=5) # organic
# Brush border (microvilli) on top wall
for xi in np.linspace(0.7, 9.3, 28):
ax.plot([xi, xi], [tube_y_hi, tube_y_hi + 0.2], color='#aaaaaa', lw=1.2, alpha=0.7)
# Reabsorption arrows (water + solute going OUT upward)
arrow_phase = (f % 12) / 12.0
arrow_alpha = 0.3 + 0.7 * abs(np.sin(np.pi * arrow_phase))
for xi in np.linspace(1.5, 8.5, 5):
ax.annotate('', xy=(xi, tube_y_hi + 0.7),
xytext=(xi, tube_y_hi + 0.15),
arrowprops=dict(arrowstyle='->', color='#88e0ef',
lw=1.8, alpha=arrow_alpha))
# Label arrows
ax.text(5, tube_y_hi + 0.95, "H₂O + Solutes reabsorbed proportionally",
ha='center', va='bottom', fontsize=8, color='#88e0ef')
# Flow arrow (left to right)
ax.annotate('', xy=(9.5, 2.5), xytext=(0.3, 2.5),
arrowprops=dict(arrowstyle='->', color='white', lw=2))
ax.text(0.3, 1.0, "125 mL/min in", fontsize=8, color='#aad4f5')
ax.text(8.5, 1.0, "44 mL/min out", fontsize=8, color='#aad4f5')
# Volume bar shrinking
vol_frac = 1 - 0.65 * phase
bar_w = 1.5 * vol_frac
ax.add_patch(mpatches.Rectangle((9.2 - bar_w, 0.3), bar_w, 0.4,
facecolor='#88e0ef', alpha=0.6))
ax.text(8.5, 0.05, "Volume", fontsize=7, color='#aad4f5', ha='center')
# Legend dots
ax.scatter([0.7], [0.5], s=28, color='#f5c518', zorder=5)
ax.text(0.95, 0.5, "NaCl", fontsize=7, color='white', va='center')
ax.scatter([1.8], [0.5], s=22, color='#88e0ef', zorder=5)
ax.text(2.05, 0.5, "H₂O", fontsize=7, color='white', va='center')
ax.scatter([2.9], [0.5], s=20, color='#c3f084', zorder=5)
ax.text(3.15, 0.5, "Glucose/AA", fontsize=7, color='white', va='center')
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg1_proximal.gif", duration=55)
print("✓ seg1_proximal.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEGMENT 2 – Descending Loop of Henle
# ══════════════════════════════════════════════════════════════════════════
def seg2_descending():
frames = []
n_frames = 40
np.random.seed(99)
for f in range(n_frames):
fig, ax = plt.subplots(figsize=(6, 5.5))
fig.patch.set_facecolor('#0d1b2a')
ax.set_facecolor('#0d1b2a')
ax.set_xlim(0, 8); ax.set_ylim(0, 7)
ax.axis('off')
ax.text(4, 6.7, "Descending Loop of Henle", ha='center', va='top',
fontsize=13, fontweight='bold', color='white')
ax.text(4, 6.25, "Permeable to H₂O, impermeable to NaCl → osmolarity RISES",
ha='center', va='top', fontsize=8, color='#aad4f5')
# Draw interstitium gradient background (left of tube)
for i, (y_bot, y_top, osm) in enumerate([
(4.5, 6.0, 300), (3.0, 4.5, 600), (1.5, 3.0, 900), (0.2, 1.5, 1200)
]):
col = osm_color(osm)
ax.add_patch(mpatches.Rectangle((0, y_bot), 3.5, y_top - y_bot,
facecolor=(*col, 0.22), edgecolor='none'))
ax.text(1.75, (y_bot + y_top) / 2, f"{osm} mOsm/L",
ha='center', va='center', fontsize=8, color='white', alpha=0.8)
ax.text(1.75, 6.5, "Interstitium", ha='center', fontsize=8,
color='#aad4f5', style='italic')
# Descending tube (right side, going down)
tube_x_lo, tube_x_hi = 4.0, 6.0
# Tubule lumen gradient (osmolarity increases as goes down)
osm_values = [300, 500, 800, 1200]
seg_ys = [(4.5, 6.0), (3.0, 4.5), (1.5, 3.0), (0.2, 1.5)]
for (y_bot, y_top), osm_val in zip(seg_ys, osm_values):
col = osm_color(osm_val)
ax.add_patch(mpatches.Rectangle((tube_x_lo, y_bot), 2, y_top - y_bot,
facecolor=(*col, 0.45), edgecolor='none'))
# Tube border
ax.add_patch(mpatches.FancyBboxPatch((tube_x_lo, 0.2), 2, 5.8,
boxstyle="round,pad=0.05",
linewidth=2, edgecolor='#aaaaaa',
facecolor='none'))
ax.text(5.0, 6.5, "Tubule\nlumen", ha='center', fontsize=8,
color='#aad4f5', style='italic')
# Osmolarity label on tube (animates value)
phase = f / n_frames
current_osm = 300 + 900 * phase
col = osm_color(current_osm)
y_dot = 6.0 - 5.8 * phase # dot descends
ax.scatter([5.0], [y_dot], s=80, color=col, zorder=8,
edgecolors='white', linewidths=1.5)
ax.text(6.3, y_dot, f"{int(current_osm)} mOsm/L",
va='center', fontsize=9, color='white', fontweight='bold')
# Water arrows pointing OUT (interstitium draws water out)
arrow_alpha = 0.4 + 0.5 * abs(np.sin(np.pi * f / 6))
for y_arr in [5.2, 3.8, 2.4, 1.0]:
ax.annotate('', xy=(3.7, y_arr), xytext=(tube_x_lo + 0.1, y_arr),
arrowprops=dict(arrowstyle='->', color='#88e0ef',
lw=2.0, alpha=arrow_alpha))
ax.text(2.0, 3.1, "H₂O\nleaves", ha='center', fontsize=8,
color='#88e0ef', alpha=0.9)
# Flow arrow (downward)
ax.annotate('', xy=(5.0, 0.5), xytext=(5.0, 5.9),
arrowprops=dict(arrowstyle='->', color='white', lw=2))
ax.text(5.0, 0.05, "→ Tip: 1200 mOsm/L", ha='center',
fontsize=8, color='#f5a623')
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg2_descending.gif", duration=60)
print("✓ seg2_descending.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEGMENT 3 – Ascending Loop of Henle
# ══════════════════════════════════════════════════════════════════════════
def seg3_ascending():
frames = []
n_frames = 40
for f in range(n_frames):
fig, ax = plt.subplots(figsize=(6, 5.5))
fig.patch.set_facecolor('#0d1b2a')
ax.set_facecolor('#0d1b2a')
ax.set_xlim(0, 8); ax.set_ylim(0, 7)
ax.axis('off')
ax.text(4, 6.7, "Ascending Loop of Henle", ha='center', va='top',
fontsize=13, fontweight='bold', color='white')
ax.text(4, 6.25, "Impermeable to H₂O — pumps NaCl OUT → fluid becomes DILUTE",
ha='center', va='top', fontsize=8, color='#aad4f5')
# Interstitium builds up
phase = f / n_frames
int_osm = 300 + 900 * (1 - phase) # from concentrated at bottom to less at top
for i, (y_bot, y_top, frac) in enumerate([
(0.2, 1.5, 1.0), (1.5, 3.0, 0.75), (3.0, 4.5, 0.5), (4.5, 6.0, 0.25)
]):
osm = 300 + 900 * frac
col = osm_color(osm)
ax.add_patch(mpatches.Rectangle((4.2, y_bot), 3.5, y_top - y_bot,
facecolor=(*col, 0.22), edgecolor='none'))
ax.text(5.9, (y_bot + y_top) / 2, f"{int(osm)} mOsm/L",
ha='center', va='center', fontsize=8, color='white', alpha=0.8)
ax.text(5.9, 6.5, "Interstitium", ha='center', fontsize=8,
color='#aad4f5', style='italic')
# Ascending tube
tube_x_lo, tube_x_hi = 1.8, 3.8
osm_values_asc = [1200, 900, 500, 200]
seg_ys = [(0.2, 1.5), (1.5, 3.0), (3.0, 4.5), (4.5, 6.0)]
for (y_bot, y_top), osm_val in zip(seg_ys, osm_values_asc):
col = osm_color(osm_val)
ax.add_patch(mpatches.Rectangle((tube_x_lo, y_bot), 2, y_top - y_bot,
facecolor=(*col, 0.45), edgecolor='none'))
ax.add_patch(mpatches.FancyBboxPatch((tube_x_lo, 0.2), 2, 5.8,
boxstyle="round,pad=0.05",
linewidth=2, edgecolor='#aaaaaa',
facecolor='none'))
# THICK ascending limb indicator
ax.add_patch(mpatches.FancyBboxPatch((tube_x_lo - 0.05, 1.5), 2.1, 4.7,
boxstyle="round,pad=0.05",
linewidth=3.5, edgecolor='#f5a623',
facecolor='none', alpha=0.6))
ax.text(2.8, 6.3, "Thick\nAscending", ha='center', fontsize=7,
color='#f5a623', style='italic')
ax.text(1.5, 6.5, "Tubule", ha='center', fontsize=8,
color='#aad4f5', style='italic')
# Animated dot going UP
y_dot = 0.2 + 5.8 * phase
current_osm = 1200 - 1100 * phase
col = osm_color(current_osm)
ax.scatter([2.8], [y_dot], s=80, color=col, zorder=8,
edgecolors='white', linewidths=1.5)
ax.text(0.3, y_dot, f"{int(current_osm)} mOsm/L",
va='center', fontsize=9, color='white', fontweight='bold')
# NaCl arrows pointing OUT (active pump, no water follows)
arrow_alpha = 0.4 + 0.5 * abs(np.sin(np.pi * f / 6))
for y_arr in [1.0, 2.2, 3.4, 4.7]:
ax.annotate('', xy=(4.5, y_arr), xytext=(tube_x_hi - 0.1, y_arr),
arrowprops=dict(arrowstyle='->', color='#f5c518',
lw=2.0, alpha=arrow_alpha))
ax.text(5.2, 2.6, "NaCl\npumped\nout", ha='center', fontsize=8,
color='#f5c518', alpha=0.9)
# NO water arrow indicator
ax.text(1.0, 3.8, "🚫 H₂O\ncannot\nfollow", ha='center', fontsize=8,
color='#ff6b6b', alpha=0.9)
# Flow arrow (upward)
ax.annotate('', xy=(2.8, 5.8), xytext=(2.8, 0.4),
arrowprops=dict(arrowstyle='->', color='white', lw=2))
ax.text(2.8, 0.0, "↑ exits at ~100 mOsm/L", ha='center',
fontsize=8, color='#88e0ef')
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg3_ascending.gif", duration=60)
print("✓ seg3_ascending.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEGMENT 4 – Distal Tubule (Diluting Segment)
# ══════════════════════════════════════════════════════════════════════════
def seg4_distal_early():
frames = []
n_frames = 36
for f in range(n_frames):
fig, ax = plt.subplots(figsize=(7, 4.5))
fig.patch.set_facecolor('#0d1b2a')
ax.set_facecolor('#0d1b2a')
ax.set_xlim(0, 10); ax.set_ylim(0, 6)
ax.axis('off')
ax.text(5, 5.6, "Early Distal Tubule (Diluting Segment)", ha='center', va='top',
fontsize=13, fontweight='bold', color='white')
ax.text(5, 5.1, "Continues to pump NaCl out — still impermeable to water",
ha='center', va='top', fontsize=8, color='#aad4f5')
tube_y_lo, tube_y_hi = 1.5, 3.5
phase = f / n_frames
# Osmolarity dropping 140 → 100
current_osm = 140 - 40 * phase
col = osm_color(current_osm)
ax.add_patch(mpatches.FancyBboxPatch((0.5, tube_y_lo), 9, tube_y_hi - tube_y_lo,
boxstyle="round,pad=0.05",
linewidth=2, edgecolor='#aaaaaa',
facecolor=(*col, 0.3)))
ax.text(5, 2.5, f"{int(current_osm)} mOsm/L", ha='center', va='center',
fontsize=14, fontweight='bold', color='white', zorder=10)
# NaCl pump arrows upward (active transport out)
arrow_alpha = 0.3 + 0.7 * abs(np.sin(np.pi * f / 5))
for xi in np.linspace(1.5, 8.5, 6):
ax.annotate('', xy=(xi, tube_y_hi + 0.65),
xytext=(xi, tube_y_hi + 0.1),
arrowprops=dict(arrowstyle='->', color='#f5c518',
lw=1.8, alpha=arrow_alpha))
ax.text(5, tube_y_hi + 0.9, "NaCl pumped out (active transport)",
ha='center', fontsize=8, color='#f5c518')
# No water cross
for xi in np.linspace(2.0, 8.0, 4):
ax.annotate('', xy=(xi, tube_y_lo - 0.6),
xytext=(xi, tube_y_lo - 0.1),
arrowprops=dict(arrowstyle='->', color='#ff6b6b',
lw=1.8, alpha=0.3))
ax.text(5, tube_y_lo - 0.8, "H₂O cannot leave (impermeable wall)",
ha='center', fontsize=8, color='#ff6b6b')
# Moving solute dots
rng = np.random.RandomState(f * 13 + 3)
n_dots = max(4, int(38 - 34 * phase)) # NaCl decreasing
xs = rng.uniform(0.7, 9.3, n_dots)
ys = rng.uniform(tube_y_lo + 0.2, tube_y_hi - 0.2, n_dots)
ax.scatter(xs, ys, s=26, color='#f5c518', alpha=0.75, zorder=5)
# Water dots (constant — not leaving)
n_water = 40
xsw = rng.uniform(0.7, 9.3, n_water)
ysw = rng.uniform(tube_y_lo + 0.2, tube_y_hi - 0.2, n_water)
ax.scatter(xsw, ysw, s=18, color='#88e0ef', alpha=0.5, zorder=4)
ax.text(0.3, 1.0, "In: ~140 mOsm/L", fontsize=8, color='#aad4f5')
ax.text(7.5, 1.0, "Out: ~100 mOsm/L", fontsize=8, color='#aad4f5')
ax.annotate('', xy=(9.5, 2.5), xytext=(0.3, 2.5),
arrowprops=dict(arrowstyle='->', color='white', lw=2))
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg4_early_distal.gif", duration=60)
print("✓ seg4_early_distal.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEGMENT 5 – Late Distal / Cortical Collecting Tubule (ADH fork)
# ══════════════════════════════════════════════════════════════════════════
def seg5_late_distal_adh():
frames = []
n_frames = 48
for f in range(n_frames):
fig, axes = plt.subplots(1, 2, figsize=(11, 5))
fig.patch.set_facecolor('#0d1b2a')
fig.suptitle("Late Distal Tubule & Cortical Collecting Duct — ADH Effect",
fontsize=12, fontweight='bold', color='white', y=0.97)
phase = f / n_frames
for col_i, (ax, has_adh) in enumerate(zip(axes, [True, False])):
ax.set_facecolor('#0d1b2a')
ax.set_xlim(0, 8); ax.set_ylim(0, 6)
ax.axis('off')
label = "HIGH ADH" if has_adh else "NO ADH"
color = '#4caf50' if has_adh else '#ef5350'
ax.text(4, 5.8, label, ha='center', va='top',
fontsize=13, fontweight='bold', color=color)
tube_y_lo, tube_y_hi = 1.5, 3.5
# Osmolarity change
if has_adh:
osm = 100 + 200 * phase # rises toward 300
water_out = True
subtitle = "Water reabsorbed → urine concentrates"
else:
osm = 100 - 50 * phase # falls to 50
water_out = False
subtitle = "Water stays → urine dilutes further"
ax.text(4, 5.2, subtitle, ha='center', fontsize=8, color='#aad4f5')
tube_col = osm_color(osm)
ax.add_patch(mpatches.FancyBboxPatch((0.5, tube_y_lo), 7, tube_y_hi - tube_y_lo,
boxstyle="round,pad=0.05",
linewidth=2, edgecolor='#aaaaaa',
facecolor=(*tube_col, 0.4)))
ax.text(4, 2.5, f"{int(osm)} mOsm/L", ha='center', va='center',
fontsize=13, fontweight='bold', color='white', zorder=10)
arrow_alpha = 0.3 + 0.7 * abs(np.sin(np.pi * f / 6))
if has_adh:
# Water leaves through aquaporins
for xi in np.linspace(1.5, 6.5, 5):
ax.annotate('', xy=(xi, tube_y_hi + 0.6),
xytext=(xi, tube_y_hi + 0.1),
arrowprops=dict(arrowstyle='->', color='#88e0ef',
lw=2, alpha=arrow_alpha))
ax.text(4, tube_y_hi + 0.85, "H₂O leaves via aquaporins",
ha='center', fontsize=8, color='#88e0ef')
# ADH receptor
ax.text(4, tube_y_lo - 0.5, "ADH → aquaporin-2 insertion",
ha='center', fontsize=8, color='#4caf50')
# Volume shrinking bar
bar_w = 1.5 * (1 - 0.5 * phase)
ax.add_patch(mpatches.Rectangle((6.5, 0.1), bar_w, 0.5,
facecolor='#88e0ef', alpha=0.6))
ax.text(6.5, 0.0, "↓ volume", fontsize=7, color='#88e0ef')
else:
# Blocked arrows (no water)
for xi in np.linspace(1.5, 6.5, 5):
ax.plot([xi, xi], [tube_y_hi + 0.1, tube_y_hi + 0.5],
color='#ff6b6b', lw=1.5, alpha=0.5, linestyle='--')
ax.text(xi, tube_y_hi + 0.6, '✗', ha='center', fontsize=9,
color='#ff6b6b', alpha=0.7)
ax.text(4, tube_y_hi + 0.85, "Water-impermeable — no aquaporins",
ha='center', fontsize=8, color='#ff6b6b')
ax.text(4, tube_y_lo - 0.5, "No ADH → large dilute urine",
ha='center', fontsize=8, color='#ef5350')
bar_w = 1.5
ax.add_patch(mpatches.Rectangle((6.5, 0.1), bar_w, 0.5,
facecolor='#88e0ef', alpha=0.3))
ax.text(6.5, 0.0, "vol same", fontsize=7, color='#88e0ef')
ax.annotate('', xy=(7.5, 2.5), xytext=(0.3, 2.5),
arrowprops=dict(arrowstyle='->', color='white', lw=2))
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg5_late_distal_adh.gif", duration=65)
print("✓ seg5_late_distal_adh.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEGMENT 6 – Medullary Collecting Duct (final urine)
# ══════════════════════════════════════════════════════════════════════════
def seg6_collecting_duct():
frames = []
n_frames = 48
for f in range(n_frames):
fig, ax = plt.subplots(figsize=(7, 5.5))
fig.patch.set_facecolor('#0d1b2a')
ax.set_facecolor('#0d1b2a')
ax.set_xlim(0, 10); ax.set_ylim(0, 7)
ax.axis('off')
ax.text(5, 6.7, "Medullary Collecting Duct", ha='center', va='top',
fontsize=14, fontweight='bold', color='white')
ax.text(5, 6.2, "Fluid equilibrates with medullary interstitium (1200 mOsm/L) when ADH present",
ha='center', va='top', fontsize=8, color='#aad4f5')
phase = f / n_frames
# Interstitium gradient (left)
for (y_bot, y_top, osm_val) in [(0.3, 1.8, 1200), (1.8, 3.3, 900),
(3.3, 4.8, 600), (4.8, 5.9, 300)]:
col = osm_color(osm_val)
ax.add_patch(mpatches.Rectangle((0, y_bot), 3.5, y_top - y_bot,
facecolor=(*col, 0.22), edgecolor='none'))
ax.text(1.75, (y_bot + y_top) / 2, f"{osm_val} mOsm/L",
ha='center', va='center', fontsize=8, color='white', alpha=0.8)
ax.text(1.75, 6.0, "Interstitium", ha='center', fontsize=8,
color='#aad4f5', style='italic')
# Collecting duct tube
tube_x_lo, tube_x_hi = 5.5, 7.5
# Two scenarios alternate with phase
# With ADH: fluid concentration rises from ~300 to 1200 as it descends
osm_top_with_adh = 300
osm_btm_with_adh = 1200
seg_ys_cd = [(4.8, 5.9), (3.3, 4.8), (1.8, 3.3), (0.3, 1.8)]
osm_segs_adh = [300, 600, 900, 1200]
osm_segs_no = [100, 80, 65, 50]
show_adh = True # always show ADH scenario; annotate both
for (y_bot, y_top), osm_val in zip(seg_ys_cd, osm_segs_adh):
col = osm_color(osm_val)
ax.add_patch(mpatches.Rectangle((tube_x_lo, y_bot), 2, y_top - y_bot,
facecolor=(*col, 0.5), edgecolor='none'))
ax.add_patch(mpatches.FancyBboxPatch((tube_x_lo, 0.3), 2, 5.6,
boxstyle="round,pad=0.05",
linewidth=2, edgecolor='#aaaaaa',
facecolor='none'))
# Animated dot descending
y_dot = 5.9 - 5.6 * phase
current_osm_adh = 300 + 900 * phase
col_dot = osm_color(current_osm_adh)
ax.scatter([6.5], [y_dot], s=100, color=col_dot, zorder=8,
edgecolors='white', linewidths=1.5)
ax.text(7.8, y_dot, f"{int(current_osm_adh)}\nmOsm/L",
va='center', fontsize=8, color='white', fontweight='bold')
# Water arrows pointing into interstitium
arrow_alpha = 0.35 + 0.6 * abs(np.sin(np.pi * f / 6))
for y_arr in [5.3, 4.0, 2.5, 1.0]:
ax.annotate('', xy=(3.7, y_arr), xytext=(tube_x_lo + 0.1, y_arr),
arrowprops=dict(arrowstyle='->', color='#88e0ef',
lw=2.2, alpha=arrow_alpha))
ax.text(2.2, 2.8, "H₂O\nleaves", ha='center', fontsize=8, color='#88e0ef')
# Urea arrows (inner medulla only — reabsorbed)
for y_arr in [1.0, 0.6]:
ax.annotate('', xy=(3.7, y_arr), xytext=(tube_x_lo + 0.1, y_arr),
arrowprops=dict(arrowstyle='->', color='#c3f084',
lw=1.5, alpha=arrow_alpha * 0.8))
ax.text(2.0, 0.55, "Urea\nrecycled", ha='center', fontsize=7, color='#c3f084')
# Flow arrow down
ax.annotate('', xy=(6.5, 0.5), xytext=(6.5, 5.7),
arrowprops=dict(arrowstyle='->', color='white', lw=2))
# Final urine label
ax.text(6.5, 0.1, "Urine: 1200 mOsm/L\n0.2 mL/min (HIGH ADH)",
ha='center', fontsize=8, color='#f5a623', fontweight='bold')
# Without ADH note
ax.text(5, 6.65, "(Without ADH: ~50 mOsm/L, 20 mL/min)",
ha='center', fontsize=7.5, color='#ef5350', style='italic')
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg6_collecting_duct.gif", duration=65)
print("✓ seg6_collecting_duct.gif")
# ── Run all ─────────────────────────────────────────────────────────────
if __name__ == '__main__':
seg1_proximal()
seg2_descending()
seg3_ascending()
seg4_distal_early()
seg5_late_distal_adh()
seg6_collecting_duct()
print("\nAll animations saved to", OUT)
cd /tmp/workspace/nephron-animations && python make_animations.py 2>&1
ls -lh /tmp/workspace/nephron-animations/*.gif
seg1 proximal
GIF File · GIF
seg2 descending
GIF File · GIF
seg3 ascending
GIF File · GIF
seg4 early distal
GIF File · GIF
seg5 late distal adh
GIF File · GIF
seg6 collecting duct
GIF File · GIF
In these animations text are overlapped and not organised generated them again properly
~/nephron-animations/make_v2.py
"""
Nephron Segment Animations v2 — clean layout, no overlapping text.
Layout principle for each frame:
┌─────────────────────────────────────────────────────┐
│ TITLE BAR (top 12% of figure) │
├─────────────────────────────────────────────────────┤
│ SUBTITLE (next 8%) │
├───────────────┬─────────────────┬───────────────────┤
│ LEFT PANEL │ TUBE / CORE │ RIGHT PANEL │
│ (labels, │ (animation) │ (values, legend) │
│ arrows) │ │ │
├───────────────┴─────────────────┴───────────────────┤
│ BOTTOM BAR (volumes / key fact) │
└─────────────────────────────────────────────────────┘
All text is placed in non-overlapping dedicated regions.
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, Rectangle
from PIL import Image
import io
import os
OUT = "/tmp/workspace/nephron-animations"
os.makedirs(OUT, exist_ok=True)
# ── colour helpers ─────────────────────────────────────────────────────────
def osm_color(osm, lo=50, hi=1200):
"""blue (dilute) → cyan → yellow → red (concentrated)"""
t = float(np.clip((osm - lo) / (hi - lo), 0, 1))
if t < 0.5:
r, g, b = 2*t, 2*t, 1.0
else:
s = (t - 0.5) * 2
r, g, b = 1.0, 1.0 - s, 0.0
return (r, g, b)
def fig_to_pil(fig):
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight',
facecolor=fig.get_facecolor())
buf.seek(0)
img = Image.open(buf).convert('RGBA')
return img.copy()
def save_gif(frames, path, duration=70):
frames[0].save(path, save_all=True, append_images=frames[1:],
duration=duration, loop=0, optimize=False)
# ── common drawing helpers ─────────────────────────────────────────────────
BG = '#111827'
TITLE_C = '#FFFFFF'
SUB_C = '#93C5FD'
ARROW_WATER = '#38BDF8'
ARROW_NACL = '#FCD34D'
ARROW_UREA = '#86EFAC'
BLOCK_C = '#1E293B'
def base_fig(title, subtitle):
fig = plt.figure(figsize=(9, 5.5))
fig.patch.set_facecolor(BG)
ax = fig.add_axes([0, 0, 1, 1])
ax.set_xlim(0, 1); ax.set_ylim(0, 1)
ax.axis('off')
ax.set_facecolor(BG)
# Title block
ax.add_patch(FancyBboxPatch((0.01, 0.88), 0.98, 0.11,
boxstyle="round,pad=0.01",
facecolor='#1E3A5F', edgecolor='#3B82F6', lw=1.5))
ax.text(0.5, 0.935, title, ha='center', va='center',
fontsize=14, fontweight='bold', color=TITLE_C, transform=ax.transAxes)
# Subtitle block
ax.add_patch(FancyBboxPatch((0.01, 0.80), 0.98, 0.075,
boxstyle="round,pad=0.01",
facecolor='#0F2744', edgecolor='none'))
ax.text(0.5, 0.838, subtitle, ha='center', va='center',
fontsize=8.5, color=SUB_C, transform=ax.transAxes)
return fig, ax
def draw_tube_h(ax, x0, x1, y_mid, height, osm, label=None):
"""Horizontal tube segment coloured by osmolarity."""
col = osm_color(osm)
y_bot = y_mid - height / 2
ax.add_patch(FancyBboxPatch((x0, y_bot), x1 - x0, height,
boxstyle="round,pad=0.008",
facecolor=(*col, 0.35),
edgecolor='#CBD5E1', lw=1.8,
transform=ax.transAxes, zorder=3))
if label:
ax.text((x0 + x1) / 2, y_mid, label, ha='center', va='center',
fontsize=11, fontweight='bold', color='white',
transform=ax.transAxes, zorder=5)
def draw_tube_v(ax, x_mid, y0, y1, width, osm_top, osm_bot, n_segs=4):
"""Vertical tube with gradient fill."""
segs = np.linspace(y0, y1, n_segs + 1)
osms = np.linspace(osm_top, osm_bot, n_segs)
for i in range(n_segs):
col = osm_color(osms[i])
ax.add_patch(Rectangle((x_mid - width/2, segs[i]), width, segs[i+1] - segs[i],
facecolor=(*col, 0.45), edgecolor='none',
transform=ax.transAxes, zorder=3))
ax.add_patch(FancyBboxPatch((x_mid - width/2, y0), width, y1 - y0,
boxstyle="round,pad=0.005",
facecolor='none', edgecolor='#CBD5E1', lw=1.8,
transform=ax.transAxes, zorder=4))
def info_box(ax, x, y, w, h, lines, colors=None):
"""Draws a dark info box with lines of text."""
ax.add_patch(FancyBboxPatch((x, y), w, h,
boxstyle="round,pad=0.01",
facecolor='#1E293B', edgecolor='#334155', lw=1,
transform=ax.transAxes, zorder=6))
n = len(lines)
for i, line in enumerate(lines):
c = colors[i] if colors else 'white'
t_y = y + h - (i + 0.7) * (h / n)
ax.text(x + w/2, t_y, line, ha='center', va='center',
fontsize=7.8, color=c, transform=ax.transAxes, zorder=7)
def pulse_alpha(f, period=10):
return 0.35 + 0.65 * abs(np.sin(np.pi * f / period))
# ══════════════════════════════════════════════════════════════════════════
# SEG 1 — Proximal Tubule
# ══════════════════════════════════════════════════════════════════════════
def seg1():
frames = []
N = 36
for f in range(N):
fig, ax = base_fig(
"Segment 1 — Proximal Tubule",
"65% of water & solutes reabsorbed proportionally | Osmolarity stays constant at 300 mOsm/L"
)
ph = f / N
osm = 300
# ── Tube (centre row: y 0.38 – 0.58) ──
draw_tube_h(ax, 0.10, 0.88, 0.48, 0.18, osm)
ax.text(0.49, 0.48, "300 mOsm/L", ha='center', va='center',
fontsize=13, fontweight='bold', color='white',
transform=ax.transAxes, zorder=5)
# Microvilli (brush border) — top of tube
for xi in np.linspace(0.12, 0.86, 22):
ax.plot([xi, xi], [0.57, 0.60], color='#94A3B8', lw=1.0,
transform=ax.transAxes, zorder=4, alpha=0.7)
# Reabsorption arrows — evenly spaced, above tube
a_alpha = pulse_alpha(f, 9)
for xi in np.linspace(0.18, 0.80, 5):
ax.annotate('', xy=(xi, 0.72), xytext=(xi, 0.60),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color=ARROW_WATER,
lw=2.0, alpha=a_alpha))
ax.annotate('', xy=(xi, 0.72), xytext=(xi + 0.04, 0.60),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color=ARROW_NACL,
lw=2.0, alpha=a_alpha))
# Arrow legend row — y 0.73 – 0.78 (above arrows, below subtitle)
ax.text(0.38, 0.76, "H₂O", ha='center', va='center', fontsize=9,
color=ARROW_WATER, transform=ax.transAxes, fontweight='bold')
ax.text(0.50, 0.76, "+ Solutes reabsorbed together → osmolarity unchanged",
ha='center', va='center', fontsize=8, color='#CBD5E1',
transform=ax.transAxes)
# Flow arrow inside tube
ax.annotate('', xy=(0.87, 0.48), xytext=(0.11, 0.48),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color='white', lw=2.2))
# Moving solute dots
rng = np.random.RandomState(f * 17 + 5)
xs_all = (rng.uniform(0.11, 0.86, 55) - ph * 0.3) % 0.75 + 0.11
ys_all = rng.uniform(0.40, 0.56, 55)
ax.scatter(xs_all[:22], ys_all[:22], s=22, color=ARROW_NACL,
alpha=0.75, zorder=5, transform=ax.transAxes)
ax.scatter(xs_all[22:44], ys_all[22:44], s=16, color=ARROW_WATER,
alpha=0.55, zorder=4, transform=ax.transAxes)
ax.scatter(xs_all[44:], ys_all[44:], s=14, color=ARROW_UREA,
alpha=0.65, zorder=4, transform=ax.transAxes)
# Bottom info boxes — two side by side
info_box(ax, 0.04, 0.04, 0.28, 0.13,
["Volume IN", "125 mL/min"], ['#94A3B8', '#38BDF8'])
info_box(ax, 0.36, 0.04, 0.28, 0.13,
["Osmolarity", "300 mOsm/L (unchanged)"], ['#94A3B8', '#FCD34D'])
info_box(ax, 0.68, 0.04, 0.28, 0.13,
["Volume OUT", "44 mL/min"], ['#94A3B8', '#38BDF8'])
# Dot legend (bottom-right corner of tube area)
for xi, col, lbl in zip([0.12, 0.22, 0.32],
[ARROW_NACL, ARROW_WATER, ARROW_UREA],
["NaCl", "H₂O", "Glucose/AA"]):
ax.scatter([xi], [0.33], s=20, color=col, transform=ax.transAxes, zorder=8)
ax.text(xi + 0.02, 0.33, lbl, fontsize=7.5, color='#CBD5E1',
va='center', transform=ax.transAxes)
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg1_proximal.gif")
print("✓ seg1_proximal.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEG 2 — Descending Loop of Henle
# ══════════════════════════════════════════════════════════════════════════
def seg2():
frames = []
N = 40
for f in range(N):
fig, ax = base_fig(
"Segment 2 — Descending Loop of Henle",
"Permeable to H₂O, impermeable to NaCl | Fluid concentrates: 300 → 1200 mOsm/L"
)
ph = f / N
# Layout zones (vertical):
# Interstitium gradient: x 0.04–0.30
# Tube: x 0.38–0.58
# Value + labels: x 0.62–0.96
# All from y 0.06 to 0.78
tube_x = 0.38; tube_w = 0.20
y_bot = 0.07; y_top = 0.78
# Interstitium gradient strips (left)
int_segs = [(0.07, 0.27, 1200), (0.27, 0.47, 900),
(0.47, 0.60, 600), (0.60, 0.78, 300)]
for yb, yt, osm in int_segs:
col = osm_color(osm)
ax.add_patch(Rectangle((0.04, yb), 0.26, yt - yb,
facecolor=(*col, 0.20), edgecolor='none',
transform=ax.transAxes))
ax.text(0.17, (yb + yt) / 2, f"{osm}", ha='center', va='center',
fontsize=9, color='white', alpha=0.85,
transform=ax.transAxes, fontweight='bold')
ax.add_patch(Rectangle((0.04, 0.07), 0.26, 0.71,
facecolor='none', edgecolor='#475569', lw=1,
transform=ax.transAxes))
ax.text(0.17, 0.82, "Interstitium", ha='center', va='bottom', fontsize=8,
color='#94A3B8', transform=ax.transAxes, style='italic')
# Tube with gradient
draw_tube_v(ax, tube_x + tube_w/2, y_bot, y_top, tube_w,
osm_top=300, osm_bot=1200, n_segs=6)
ax.text(tube_x + tube_w/2, 0.82, "Tubule", ha='center', va='bottom',
fontsize=8, color='#94A3B8', transform=ax.transAxes, style='italic')
# Animated dot descending
y_dot = y_top - (y_top - y_bot) * ph
cur_osm = 300 + 900 * ph
col_dot = osm_color(cur_osm)
ax.scatter([tube_x + tube_w/2], [y_dot], s=110, color=col_dot,
edgecolors='white', linewidths=1.5,
transform=ax.transAxes, zorder=8)
# Water arrows (tube → interstitium, pointing LEFT)
a_alpha = pulse_alpha(f, 8)
for y_arr in [0.17, 0.32, 0.47, 0.62, 0.72]:
ax.annotate('', xy=(0.33, y_arr), xytext=(tube_x + 0.01, y_arr),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color=ARROW_WATER,
lw=2.0, alpha=a_alpha))
# Right panel — value boxes
info_box(ax, 0.62, 0.60, 0.34, 0.16,
["Osmolarity now", f"{int(cur_osm)} mOsm/L"],
['#94A3B8', '#FCD34D'])
info_box(ax, 0.62, 0.38, 0.34, 0.16,
["H₂O exits by", "osmosis (passive)"],
['#94A3B8', ARROW_WATER])
info_box(ax, 0.62, 0.16, 0.34, 0.16,
["NaCl stays", "wall impermeable"],
['#94A3B8', '#F87171'])
# Bottom bar
ax.add_patch(Rectangle((0.01, 0.0), 0.98, 0.055,
facecolor='#0F172A', edgecolor='none',
transform=ax.transAxes))
ax.text(0.5, 0.027, "Tip of loop = 1200 mOsm/L | Volume: 44 → 25 mL/min",
ha='center', va='center', fontsize=8.5, color='#FCD34D',
fontweight='bold', transform=ax.transAxes)
# Flow arrow (downward, inside tube)
ax.annotate('', xy=(tube_x + tube_w/2, y_bot + 0.04),
xytext=(tube_x + tube_w/2, y_top - 0.04),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color='white', lw=1.8, alpha=0.5))
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg2_descending.gif")
print("✓ seg2_descending.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEG 3 — Ascending Loop (Thick)
# ══════════════════════════════════════════════════════════════════════════
def seg3():
frames = []
N = 40
for f in range(N):
fig, ax = base_fig(
"Segment 3 — Thick Ascending Loop of Henle (Diluting Segment)",
"Active NaCl pump OUT | Impermeable to water | Osmolarity falls: 1200 → 100 mOsm/L"
)
ph = f / N
tube_x = 0.38; tube_w = 0.20
y_bot = 0.07; y_top = 0.78
# Interstitium (right side) — built up by NaCl pumping
int_segs = [(0.07, 0.27, 1200), (0.27, 0.47, 900),
(0.47, 0.60, 600), (0.60, 0.78, 300)]
for yb, yt, osm in int_segs:
col = osm_color(osm)
ax.add_patch(Rectangle((0.66, yb), 0.30, yt - yb,
facecolor=(*col, 0.20), edgecolor='none',
transform=ax.transAxes))
ax.text(0.81, (yb + yt) / 2, f"{osm}", ha='center', va='center',
fontsize=9, color='white', alpha=0.85,
transform=ax.transAxes, fontweight='bold')
ax.add_patch(Rectangle((0.66, 0.07), 0.30, 0.71,
facecolor='none', edgecolor='#475569', lw=1,
transform=ax.transAxes))
ax.text(0.81, 0.82, "Interstitium", ha='center', va='bottom', fontsize=8,
color='#94A3B8', transform=ax.transAxes, style='italic')
# Tube with gradient — ascending so osm_top=1200, osm_bot (display)=100
draw_tube_v(ax, tube_x + tube_w/2, y_bot, y_top, tube_w,
osm_top=1200, osm_bot=100, n_segs=6)
# Thick ascending limb orange border
ax.add_patch(FancyBboxPatch((tube_x - 0.01, y_bot + 0.15),
tube_w + 0.02, (y_top - y_bot) * 0.85,
boxstyle="round,pad=0.005",
facecolor='none', edgecolor='#F97316',
lw=2.5, transform=ax.transAxes,
zorder=4, alpha=0.7))
ax.text(tube_x + tube_w/2, 0.82, "Tubule", ha='center', va='bottom',
fontsize=8, color='#94A3B8', transform=ax.transAxes, style='italic')
# Animated dot going UP
y_dot = y_bot + (y_top - y_bot) * ph
cur_osm = 1200 - 1100 * ph
col_dot = osm_color(cur_osm)
ax.scatter([tube_x + tube_w/2], [y_dot], s=110, color=col_dot,
edgecolors='white', linewidths=1.5,
transform=ax.transAxes, zorder=8)
# NaCl arrows pointing RIGHT (out into interstitium)
a_alpha = pulse_alpha(f, 8)
for y_arr in [0.17, 0.32, 0.47, 0.62, 0.72]:
ax.annotate('', xy=(0.63, y_arr), xytext=(tube_x + tube_w + 0.01, y_arr),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color=ARROW_NACL,
lw=2.0, alpha=a_alpha))
# Left panel: info boxes
info_box(ax, 0.03, 0.60, 0.33, 0.16,
["Osmolarity now", f"{int(cur_osm)} mOsm/L"],
['#94A3B8', '#FCD34D'])
info_box(ax, 0.03, 0.38, 0.33, 0.16,
["NaCl pumped OUT", "(active transport)"],
['#94A3B8', ARROW_NACL])
info_box(ax, 0.03, 0.16, 0.33, 0.16,
["H₂O cannot follow", "wall is IMPERMEABLE"],
['#94A3B8', '#F87171'])
# Bottom bar
ax.add_patch(Rectangle((0.01, 0.0), 0.98, 0.055,
facecolor='#0F172A', edgecolor='none',
transform=ax.transAxes))
ax.text(0.5, 0.027,
"Exits distal tubule at ~100 mOsm/L (always dilute — regardless of ADH)",
ha='center', va='center', fontsize=8.5, color='#38BDF8',
fontweight='bold', transform=ax.transAxes)
# Flow arrow (upward)
ax.annotate('', xy=(tube_x + tube_w/2, y_top - 0.04),
xytext=(tube_x + tube_w/2, y_bot + 0.04),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color='white', lw=1.8, alpha=0.5))
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg3_ascending.gif")
print("✓ seg3_ascending.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEG 4 — Early Distal Tubule
# ══════════════════════════════════════════════════════════════════════════
def seg4():
frames = []
N = 36
for f in range(N):
fig, ax = base_fig(
"Segment 4 — Early Distal Tubule (Diluting Segment continued)",
"Same as ascending loop: NaCl pumped out, water stays | 140 → 100 mOsm/L"
)
ph = f / N
cur_osm = 140 - 40 * ph
col_t = osm_color(cur_osm)
# Tube
draw_tube_h(ax, 0.10, 0.88, 0.48, 0.20, cur_osm)
ax.text(0.49, 0.48, f"{int(cur_osm)} mOsm/L", ha='center', va='center',
fontsize=14, fontweight='bold', color='white',
transform=ax.transAxes, zorder=5)
# NaCl arrows ABOVE tube (pumped out)
a_alpha = pulse_alpha(f, 9)
for xi in np.linspace(0.18, 0.80, 5):
ax.annotate('', xy=(xi, 0.73), xytext=(xi, 0.60),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color=ARROW_NACL,
lw=2.0, alpha=a_alpha))
ax.text(0.49, 0.77, "NaCl pumped OUT (active transport)",
ha='center', va='center', fontsize=9, color=ARROW_NACL,
transform=ax.transAxes)
# Blocked water arrows BELOW tube
for xi in np.linspace(0.18, 0.80, 5):
ax.plot([xi, xi], [0.36, 0.26], color='#F87171', lw=1.5,
linestyle='--', transform=ax.transAxes, alpha=0.6)
ax.text(xi, 0.24, 'X', ha='center', va='top', fontsize=9,
color='#F87171', transform=ax.transAxes, alpha=0.8)
ax.text(0.49, 0.19, "H₂O BLOCKED (wall impermeable)",
ha='center', va='center', fontsize=9, color='#F87171',
transform=ax.transAxes)
# Flow arrow
ax.annotate('', xy=(0.87, 0.48), xytext=(0.11, 0.48),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color='white', lw=2.2))
# Moving solute dots inside tube
rng = np.random.RandomState(f * 11)
n_dots = max(6, int(40 - 30 * ph))
xs = (rng.uniform(0.11, 0.86, n_dots) - ph * 0.2) % 0.75 + 0.11
ys = rng.uniform(0.40, 0.56, n_dots)
ax.scatter(xs, ys, s=22, color=ARROW_NACL, alpha=0.7, zorder=5,
transform=ax.transAxes)
# Bottom info strip
info_box(ax, 0.04, 0.02, 0.28, 0.12,
["Entering", "~140 mOsm/L"], ['#94A3B8', '#FCD34D'])
info_box(ax, 0.36, 0.02, 0.28, 0.12,
["Segment role", "Diluting"], ['#94A3B8', '#38BDF8'])
info_box(ax, 0.68, 0.02, 0.28, 0.12,
["Exiting", "~100 mOsm/L"], ['#94A3B8', '#38BDF8'])
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg4_early_distal.gif")
print("✓ seg4_early_distal.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEG 5 — Late Distal + Cortical Collecting (ADH fork)
# ══════════════════════════════════════════════════════════════════════════
def seg5():
frames = []
N = 48
for f in range(N):
fig = plt.figure(figsize=(12, 6))
fig.patch.set_facecolor(BG)
# Title
fig.text(0.5, 0.96, "Segment 5 — Late Distal & Cortical Collecting Duct | ADH makes all the difference",
ha='center', va='top', fontsize=12, fontweight='bold', color=TITLE_C)
fig.text(0.5, 0.90, "Both sides receive the same fluid (~100 mOsm/L) — outcome depends entirely on ADH level",
ha='center', va='top', fontsize=8.5, color=SUB_C)
ph = f / N
a_alpha = pulse_alpha(f, 8)
for col_i, (x_off, has_adh) in enumerate([(0.03, True), (0.52, False)]):
ax = fig.add_axes([x_off, 0.04, 0.46, 0.84])
ax.set_xlim(0, 1); ax.set_ylim(0, 1)
ax.axis('off')
ax.set_facecolor(BG)
label = "HIGH ADH" if has_adh else "NO ADH"
label_c = '#4ADE80' if has_adh else '#F87171'
osm = (100 + 200 * ph) if has_adh else (100 - 50 * ph)
col_t = osm_color(osm)
# Panel background tint
ax.add_patch(Rectangle((0, 0), 1, 1,
facecolor='#4ADE8008' if has_adh else '#F8717108',
edgecolor=label_c, lw=2,
transform=ax.transAxes))
# Panel title
ax.add_patch(FancyBboxPatch((0.02, 0.88), 0.96, 0.10,
boxstyle="round,pad=0.01",
facecolor='#1E3A5F' if has_adh else '#3B1E1E',
edgecolor=label_c, lw=1.5,
transform=ax.transAxes))
ax.text(0.5, 0.93, label, ha='center', va='center',
fontsize=14, fontweight='bold', color=label_c,
transform=ax.transAxes)
# Tube
draw_tube_h(ax, 0.08, 0.92, 0.52, 0.20, osm)
ax.text(0.5, 0.52, f"{int(osm)} mOsm/L", ha='center', va='center',
fontsize=13, fontweight='bold', color='white',
transform=ax.transAxes, zorder=5)
if has_adh:
# Water arrows UP (exits via aquaporins)
for xi in np.linspace(0.15, 0.85, 5):
ax.annotate('', xy=(xi, 0.79), xytext=(xi, 0.64),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color=ARROW_WATER,
lw=2.2, alpha=a_alpha))
ax.text(0.5, 0.84, "H₂O exits via aquaporin-2",
ha='center', va='center', fontsize=9,
color=ARROW_WATER, transform=ax.transAxes)
# Info boxes
info_box(ax, 0.05, 0.26, 0.42, 0.14,
["ADH opens", "aquaporins (AQP2)"], ['#94A3B8', '#4ADE80'])
info_box(ax, 0.53, 0.26, 0.42, 0.14,
["Volume shrinks", "urine concentrates"], ['#94A3B8', '#FCD34D'])
else:
# Blocked arrows
for xi in np.linspace(0.15, 0.85, 5):
ax.plot([xi, xi], [0.64, 0.79], color='#F87171', lw=2,
linestyle='--', transform=ax.transAxes, alpha=0.7)
ax.text(xi, 0.81, 'X', ha='center', va='bottom',
fontsize=10, color='#F87171',
transform=ax.transAxes)
ax.text(0.5, 0.85, "No aquaporins inserted — wall impermeable",
ha='center', va='center', fontsize=8.5,
color='#F87171', transform=ax.transAxes)
info_box(ax, 0.05, 0.26, 0.42, 0.14,
["No aquaporins", "water stays in"], ['#94A3B8', '#F87171'])
info_box(ax, 0.53, 0.26, 0.42, 0.14,
["Large dilute urine", "produced"], ['#94A3B8', '#38BDF8'])
# Flow arrow
ax.annotate('', xy=(0.91, 0.52), xytext=(0.09, 0.52),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color='white', lw=2.0))
# Bottom strip
final = "~300 mOsm/L | 8 mL/min" if has_adh else "~50 mOsm/L | 20 mL/min"
ax.add_patch(Rectangle((0.01, 0.01), 0.98, 0.12,
facecolor='#0F172A', edgecolor='none',
transform=ax.transAxes))
ax.text(0.5, 0.07, f"Exits as: {final}",
ha='center', va='center', fontsize=9,
color='#FCD34D', fontweight='bold',
transform=ax.transAxes)
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg5_adh_fork.gif")
print("✓ seg5_adh_fork.gif")
# ══════════════════════════════════════════════════════════════════════════
# SEG 6 — Medullary Collecting Duct
# ══════════════════════════════════════════════════════════════════════════
def seg6():
frames = []
N = 48
for f in range(N):
fig, ax = base_fig(
"Segment 6 — Medullary Collecting Duct (Final Concentration)",
"With ADH: fluid equilibrates with 1200 mOsm/L medullary interstitium | Urea recycled here"
)
ph = f / N
tube_x = 0.38; tube_w = 0.20
y_bot = 0.07; y_top = 0.78
# Interstitium (left)
int_segs = [(0.07, 0.27, 1200), (0.27, 0.47, 900),
(0.47, 0.60, 600), (0.60, 0.78, 300)]
for yb, yt, osm in int_segs:
col = osm_color(osm)
ax.add_patch(Rectangle((0.04, yb), 0.28, yt - yb,
facecolor=(*col, 0.20), edgecolor='none',
transform=ax.transAxes))
ax.text(0.18, (yb + yt) / 2, f"{osm}", ha='center', va='center',
fontsize=9, color='white', alpha=0.85,
transform=ax.transAxes, fontweight='bold')
ax.add_patch(Rectangle((0.04, 0.07), 0.28, 0.71,
facecolor='none', edgecolor='#475569', lw=1,
transform=ax.transAxes))
ax.text(0.18, 0.82, "Interstitium", ha='center', va='bottom', fontsize=8,
color='#94A3B8', transform=ax.transAxes, style='italic')
# Tube — starts ~300, ends ~1200 (with ADH)
draw_tube_v(ax, tube_x + tube_w/2, y_bot, y_top, tube_w,
osm_top=300, osm_bot=1200, n_segs=6)
ax.text(tube_x + tube_w/2, 0.82, "Collecting\nDuct", ha='center', va='bottom',
fontsize=8, color='#94A3B8', transform=ax.transAxes, style='italic')
# Animated dot
y_dot = y_top - (y_top - y_bot) * ph
cur_osm = 300 + 900 * ph
col_dot = osm_color(cur_osm)
ax.scatter([tube_x + tube_w/2], [y_dot], s=120, color=col_dot,
edgecolors='white', linewidths=1.5,
transform=ax.transAxes, zorder=8)
# Water + urea arrows pointing LEFT (into interstitium)
a_alpha = pulse_alpha(f, 8)
for y_arr in [0.17, 0.30, 0.44, 0.58, 0.70]:
ax.annotate('', xy=(0.35, y_arr), xytext=(tube_x + 0.01, y_arr),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color=ARROW_WATER,
lw=2.0, alpha=a_alpha))
# Urea arrows (inner medulla — lower portion only)
for y_arr in [0.12, 0.20]:
ax.annotate('', xy=(0.35, y_arr), xytext=(tube_x + 0.01, y_arr),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color=ARROW_UREA,
lw=1.8, alpha=a_alpha * 0.9))
# Right panel info boxes — stacked cleanly
info_box(ax, 0.63, 0.62, 0.34, 0.14,
["Osmolarity now", f"{int(cur_osm)} mOsm/L"],
['#94A3B8', '#FCD34D'])
info_box(ax, 0.63, 0.44, 0.34, 0.14,
["H₂O exits", "(aquaporins, ADH-driven)"],
['#94A3B8', ARROW_WATER])
info_box(ax, 0.63, 0.26, 0.34, 0.14,
["Urea recycled", "via UTA-1 / UTA-3"],
['#94A3B8', ARROW_UREA])
info_box(ax, 0.63, 0.08, 0.34, 0.14,
["Without ADH:", "~50 mOsm/L, 20 mL/min"],
['#F87171', '#F87171'])
# Flow arrow (downward)
ax.annotate('', xy=(tube_x + tube_w/2, y_bot + 0.03),
xytext=(tube_x + tube_w/2, y_top - 0.04),
xycoords='axes fraction', textcoords='axes fraction',
arrowprops=dict(arrowstyle='->', color='white', lw=1.8, alpha=0.5))
# Bottom bar
ax.add_patch(Rectangle((0.01, 0.0), 0.98, 0.055,
facecolor='#0F172A', edgecolor='none',
transform=ax.transAxes))
ax.text(0.5, 0.027,
"FINAL URINE (HIGH ADH): 1200 mOsm/L | 0.2 mL/min (concentrated!)",
ha='center', va='center', fontsize=8.5, color='#F97316',
fontweight='bold', transform=ax.transAxes)
frames.append(fig_to_pil(fig))
plt.close(fig)
save_gif(frames, f"{OUT}/seg6_collecting_duct.gif")
print("✓ seg6_collecting_duct.gif")
# ── Run all ────────────────────────────────────────────────────────────────
if __name__ == '__main__':
seg1()
seg2()
seg3()
seg4()
seg5()
seg6()
print("\nAll v2 animations saved to", OUT)
cd /tmp/workspace/nephron-animations && python make_v2.py 2>&1
ls -lh /tmp/workspace/nephron-animations/*.gif
seg1 proximal
GIF File · GIF
seg2 descending
GIF File · GIF
seg3 ascending
GIF File · GIF
seg4 early distal
GIF File · GIF
seg5 adh fork
GIF File · GIF
seg6 collecting duct
GIF File · GIF
Does ADH act on PCT
"In the proximal tubule and descending loop of Henle, water permeability is always high, and water is rapidly reabsorbed to reach osmotic equilibrium with the surrounding interstitial fluid. This high permeability is due to abundant expression of AQP-1 in the luminal and basolateral membranes."
- Guyton and Hall Textbook of Medical Physiology
| Segment | ADH effect |
|---|---|
| Late distal tubule | Inserts AQP-2 into luminal membrane → becomes water-permeable |
| Cortical collecting tubule | Same - AQP-2 insertion |
| Medullary collecting duct | AQP-2 insertion → fluid equilibrates with 1200 mOsm/L interstitium |
| PCT | No effect - already always permeable via AQP-1 |
| Ascending loop of Henle | No effect - permanently water-impermeable |
Does adh act of loop of henle
"In the ascending loop of Henle, water permeability is always low, so almost no water is reabsorbed, despite a large osmotic gradient."
"Water permeability in the last parts of the tubules - the distal tubules, collecting tubules, and collecting ducts - can be high or low, depending on the presence or absence of ADH."
| Segment | Water permeability | ADH effect? |
|---|---|---|
| PCT | Always HIGH (AQP-1) | None |
| Descending loop | Always HIGH (AQP-1) | None |
| Ascending loop | Always LOW (no AQP) | None |
| Early distal tubule | Always LOW | None |
| Late distal tubule | Variable | Yes - AQP-2 |
| Cortical collecting duct | Variable | Yes - AQP-2 |
| Medullary collecting duct | Variable | Yes - AQP-2 |
So why is osmolarity changes depending on ADH secretion
Collecting duct fluid: 100 mOsm/L
Medullary interstitium: 1200 mOsm/L
← enormous osmotic gradient exists across the wall →
HIGH ADH → AQP-2 channels inserted → gate OPEN
Water rushes out by osmosis into the interstitium
Fluid concentrates to 1200 mOsm/L
Volume shrinks to 0.2 mL/min
→ CONCENTRATED urine
NO ADH → no AQP-2 → gate SHUT
Water cannot cross the wall despite the gradient
NaCl continues to be reabsorbed but water stays
Osmolarity drops to ~50 mOsm/L
Volume stays ~20 mL/min
→ DILUTE urine
| State | ADH | Outcome |
|---|---|---|
| Dehydrated / low blood volume | High ADH | Kidneys conserve water → concentrated urine |
| Overhydrated / high blood volume | Low/no ADH | Kidneys excrete excess water → dilute urine |