~/pharmacokinetics/pk_timetable.py
import openpyxl
from openpyxl.styles import (
PatternFill, Font, Alignment, Border, Side, GradientFill
)
from openpyxl.utils import get_column_letter
from openpyxl.chart import LineChart, Reference
from openpyxl.chart.series import DataPoint
import math
wb = openpyxl.Workbook()
# ─── COLOURS ───────────────────────────────────────────────────────────────
DARK_BLUE = "1F4E79"
MED_BLUE = "2E75B6"
LIGHT_BLUE = "BDD7EE"
HEADER_BG = "1F4E79"
ALT_ROW = "EBF3FB"
WHITE = "FFFFFF"
GREEN = "375623"
GREEN_BG = "E2EFDA"
ORANGE_BG = "FCE4D6"
ORANGE = "833C00"
YELLOW_BG = "FFF2CC"
PURPLE_BG = "E9D7F5"
RED = "C00000"
GREY_BG = "F2F2F2"
def hdr_font(size=11, bold=True, color=WHITE):
return Font(name="Arial", size=size, bold=bold, color=color)
def body_font(size=10, bold=False, color="000000"):
return Font(name="Arial", size=size, bold=bold, color=color)
def fill(hex_color):
return PatternFill("solid", fgColor=hex_color)
def thin_border():
s = Side(style="thin", color="AAAAAA")
return Border(left=s, right=s, top=s, bottom=s)
def center():
return Alignment(horizontal="center", vertical="center", wrap_text=True)
def left():
return Alignment(horizontal="left", vertical="center", wrap_text=True)
def merge_title(ws, cell_range, text, bg, fg=WHITE, size=13, bold=True):
ws.merge_cells(cell_range)
c = ws[cell_range.split(":")[0]]
c.value = text
c.font = Font(name="Arial", size=size, bold=bold, color=fg)
c.fill = fill(bg)
c.alignment = center()
# ══════════════════════════════════════════════════════════════════════════════
# SHEET 1 — FORMULAS & CONCEPTS
# ══════════════════════════════════════════════════════════════════════════════
ws1 = wb.active
ws1.title = "Formulas & Concepts"
ws1.sheet_view.showGridLines = False
ws1.column_dimensions["A"].width = 28
ws1.column_dimensions["B"].width = 32
ws1.column_dimensions["C"].width = 36
ws1.column_dimensions["D"].width = 38
# Title
ws1.row_dimensions[1].height = 36
merge_title(ws1, "A1:D1",
"PHARMACOKINETICS — Formulas & Key Concepts",
DARK_BLUE, WHITE, 14)
ws1.row_dimensions[2].height = 8 # spacer
# Sub-headers
headers = ["Parameter", "Formula", "What You Need", "Interpretation"]
for col, h in enumerate(headers, 1):
c = ws1.cell(row=3, column=col, value=h)
c.font = hdr_font(11)
c.fill = fill(MED_BLUE)
c.alignment = center()
c.border = thin_border()
ws1.row_dimensions[3].height = 22
rows = [
("Volume of Distribution\n(Vd)",
"Vd = Dose ÷ C₀\n\nLoading Dose = Vd × C_target",
"Dose (mg)\nC₀ = plasma conc. at time 0 (mg/L)",
"Small Vd → stays in plasma\nLarge Vd → hides in tissues\nVd > total body volume → extensive tissue binding"),
("Clearance\n(CL)",
"CL = Dose ÷ AUC\nCL = Ke × Vd\n\nCL_total = CL_renal + CL_hepatic",
"Dose (mg)\nAUC = area under conc-time curve (mg·hr/L)",
"How efficiently the body removes drug\nHigh CL → rapid elimination\nLow CL → drug accumulates"),
("Elimination Rate Constant\n(Ke)",
"Ke = CL ÷ Vd\nKe = 0.693 ÷ t½",
"CL (L/hr)\nVd (L)",
"Fraction of drug eliminated per unit time\nUnits: hr⁻¹\nSlope of log(C) vs time graph"),
("Half-Life\n(t½)",
"t½ = 0.693 × Vd ÷ CL\nt½ = 0.693 ÷ Ke",
"Vd (L)\nCL (L/hr)\nOR Ke (hr⁻¹)",
"Time for plasma conc to fall by 50%\n↑Vd → ↑t½\n↑CL → ↓t½\nSteady state reached after ~5 × t½"),
("Concentration at Time t\n[C(t)]",
"C(t) = C₀ × e^(−Ke × t)",
"C₀ (mg/L)\nKe (hr⁻¹)\nt = time point (hr)",
"Predicts plasma level at any time\nAfter 1 t½: 50% of C₀\nAfter 2 t½: 25% of C₀\nAfter n t½: C₀ × (0.5)ⁿ"),
("AUC (Area Under Curve)",
"AUC = C₀ ÷ Ke\nAUC = C₀ × t½ ÷ 0.693",
"C₀ (mg/L)\nKe (hr⁻¹)\nOR C₀ + t½",
"Total drug exposure\nUsed to calculate CL\nLarger AUC = more drug exposure"),
]
bg_colors = [LIGHT_BLUE, GREEN_BG, YELLOW_BG, ORANGE_BG, PURPLE_BG, GREY_BG]
for r_idx, (row, bg) in enumerate(zip(rows, bg_colors), 4):
ws1.row_dimensions[r_idx].height = 68
for col_idx, val in enumerate(row, 1):
c = ws1.cell(row=r_idx, column=col_idx, value=val)
c.fill = fill(bg)
c.border = thin_border()
c.alignment = left() if col_idx > 1 else center()
if col_idx == 1:
c.font = Font(name="Arial", size=10, bold=True, color=DARK_BLUE)
elif col_idx == 2:
c.font = Font(name="Arial", size=10, bold=False, color="000000")
else:
c.font = body_font(9)
# ══════════════════════════════════════════════════════════════════════════════
# SHEET 2 — WORKED EXAMPLES
# ══════════════════════════════════════════════════════════════════════════════
ws2 = wb.create_sheet("Worked Examples")
ws2.sheet_view.showGridLines = False
for col, w in zip("ABCDE", [6, 28, 22, 32, 32]):
ws2.column_dimensions[col].width = w
ws2.row_dimensions[1].height = 36
merge_title(ws2, "A1:E1", "PHARMACOKINETICS — Worked Examples (3 Drugs)", DARK_BLUE)
ws2.row_dimensions[2].height = 8
# ── Example drugs data ──────────────────────────────────────────────────────
drugs = [
{
"name": "Drug A — Amoxicillin",
"color": GREEN_BG, "hdr_color": "375623",
"dose": 500, "C0": 15.15, "AUC": 1.2,
"note": "Antibiotic; renally cleared; short t½ → dose every 8 hrs",
},
{
"name": "Drug B — Phenytoin",
"color": ORANGE_BG, "hdr_color": ORANGE,
"dose": 300, "C0": 7.14, "AUC": 60,
"note": "Antiepileptic; hepatically cleared; zero-order at high doses",
},
{
"name": "Drug C — Digoxin",
"color": PURPLE_BG, "hdr_color": "4B0082",
"dose": 0.5, "C0": 0.001, "AUC": 0.1,
"note": "Cardiac glycoside; large Vd due to tissue binding; renally cleared",
},
]
row = 3
for drug in drugs:
dose = drug["dose"]
C0 = drug["C0"]
AUC = drug["AUC"]
Vd = round(dose / C0, 2)
CL = round(dose / AUC, 4)
Ke = round(CL / Vd, 4)
t_half = round(0.693 / Ke, 2)
# Drug header
ws2.row_dimensions[row].height = 22
ws2.merge_cells(f"A{row}:E{row}")
c = ws2[f"A{row}"]
c.value = drug["name"]
c.font = Font(name="Arial", size=12, bold=True, color=drug["hdr_color"])
c.fill = fill(drug["color"])
c.alignment = center()
row += 1
# Column headers
ws2.row_dimensions[row].height = 20
for col, h in enumerate(["Step", "What We're Calculating", "Formula Used", "Calculation", "Result"], 1):
c = ws2.cell(row=row, column=col, value=h)
c.font = hdr_font(10)
c.fill = fill(MED_BLUE)
c.alignment = center()
c.border = thin_border()
row += 1
steps = [
("1", "Volume of Distribution (Vd)",
"Vd = Dose ÷ C₀",
f"{dose} mg ÷ {C0} mg/L",
f"Vd = {Vd} L"),
("2", "Clearance (CL)",
"CL = Dose ÷ AUC",
f"{dose} mg ÷ {AUC} mg·hr/L",
f"CL = {CL} L/hr"),
("3", "Elimination Rate Constant (Ke)",
"Ke = CL ÷ Vd",
f"{CL} ÷ {Vd}",
f"Ke = {Ke} hr⁻¹"),
("4", "Half-Life (t½)",
"t½ = 0.693 ÷ Ke",
f"0.693 ÷ {Ke}",
f"t½ = {t_half} hr"),
("5", "AUC Verification",
"AUC = C₀ ÷ Ke",
f"{C0} ÷ {Ke}",
f"AUC ≈ {round(C0/Ke, 2)} mg·hr/L"),
]
for step in steps:
ws2.row_dimensions[row].height = 22
for col, val in enumerate(step, 1):
c = ws2.cell(row=row, column=col, value=val)
c.font = body_font(10, bold=(col == 5))
c.fill = fill(drug["color"])
c.border = thin_border()
c.alignment = center() if col in (1, 3, 5) else left()
row += 1
# Note row
ws2.row_dimensions[row].height = 20
ws2.merge_cells(f"A{row}:E{row}")
c = ws2[f"A{row}"]
c.value = f"📋 Note: {drug['note']}"
c.font = Font(name="Arial", size=9, italic=True, color="555555")
c.fill = fill(GREY_BG)
c.alignment = left()
row += 2 # spacer
# ══════════════════════════════════════════════════════════════════════════════
# SHEET 3 — ELIMINATION TIMETABLE (3 drugs)
# ══════════════════════════════════════════════════════════════════════════════
ws3 = wb.create_sheet("Elimination Timetable")
ws3.sheet_view.showGridLines = False
# Column widths
ws3.column_dimensions["A"].width = 14 # Time (hr)
ws3.column_dimensions["B"].width = 10 # # half-lives
ws3.column_dimensions["C"].width = 18 # Amoxicillin C
ws3.column_dimensions["D"].width = 16 # Amox % remain
ws3.column_dimensions["E"].width = 18 # Phenytoin C
ws3.column_dimensions["F"].width = 16 # Phenyt % remain
ws3.column_dimensions["G"].width = 18 # Digoxin C
ws3.column_dimensions["H"].width = 16 # Digoxin % remain
ws3.row_dimensions[1].height = 36
merge_title(ws3, "A1:H1",
"ELIMINATION TIMETABLE — Plasma Concentration Over Time (First-Order Kinetics)",
DARK_BLUE)
ws3.row_dimensions[2].height = 8
# Drug params for timetable
# Amox: C0=15.15 mg/L, t½=1.2 hr, Ke=0.577
# Phenytoin: C0=7.14 mg/L, t½=14 hr, Ke=0.0495
# Digoxin: C0=0.001 mg/L (1 µg/L), t½=40 hr, Ke=0.01733
drugs_t = [
{"name": "Amoxicillin", "C0": 15.15, "t_half": 1.2, "Ke": round(0.693/1.2, 4), "unit": "mg/L", "dose": "500 mg"},
{"name": "Phenytoin", "C0": 7.14, "t_half": 14.0, "Ke": round(0.693/14.0, 4), "unit": "mg/L", "dose": "300 mg"},
{"name": "Digoxin", "C0": 1.0, "t_half": 40.0, "Ke": round(0.693/40.0, 4), "unit": "µg/L", "dose": "0.5 mg"},
]
# Drug sub-headers row 3
ws3.row_dimensions[3].height = 16
ws3.merge_cells("C3:D3")
ws3["C3"].value = f"Amoxicillin (t½={drugs_t[0]['t_half']} hr)"
ws3["C3"].font = Font(name="Arial", size=10, bold=True, color=GREEN)
ws3["C3"].fill = fill(GREEN_BG)
ws3["C3"].alignment = center()
ws3.merge_cells("E3:F3")
ws3["E3"].value = f"Phenytoin (t½={drugs_t[1]['t_half']} hr)"
ws3["E3"].font = Font(name="Arial", size=10, bold=True, color=ORANGE)
ws3["E3"].fill = fill(ORANGE_BG)
ws3["E3"].alignment = center()
ws3.merge_cells("G3:H3")
ws3["G3"].value = f"Digoxin (t½={drugs_t[2]['t_half']} hr)"
ws3["G3"].font = Font(name="Arial", size=10, bold=True, color="4B0082")
ws3["G3"].fill = fill(PURPLE_BG)
ws3["G3"].alignment = center()
# Column headers row 4
ws3.row_dimensions[4].height = 22
col_headers = [
"Time (hr)", "# Half-Lives",
f"Conc ({drugs_t[0]['unit']})", "% Remaining",
f"Conc ({drugs_t[1]['unit']})", "% Remaining",
f"Conc ({drugs_t[2]['unit']})", "% Remaining",
]
col_fills = [DARK_BLUE, DARK_BLUE, MED_BLUE, MED_BLUE, "A0522D", "A0522D", "4B0082", "4B0082"]
for col, (h, bg) in enumerate(zip(col_headers, col_fills), 1):
c = ws3.cell(row=4, column=col, value=h)
c.font = hdr_font(10)
c.fill = fill(bg)
c.alignment = center()
c.border = thin_border()
# Time points — smart: covers 5 half-lives of the longest drug (digoxin, 40 hr → 200 hr)
time_points = [0, 1, 2, 4, 6, 8, 12, 14, 20, 24, 28, 40, 48, 56, 60, 80, 100, 120, 140, 160, 200]
row_colors = [WHITE, ALT_ROW]
for r_idx, t in enumerate(time_points, 5):
ws3.row_dimensions[r_idx].height = 18
bg = row_colors[r_idx % 2]
n_half_lives_amox = round(t / drugs_t[0]["t_half"], 2)
row_data = [t, n_half_lives_amox]
for drug in drugs_t:
C0 = drug["C0"]
Ke = drug["Ke"]
Ct = round(C0 * math.exp(-Ke * t), 4)
pct = round((Ct / C0) * 100, 2)
row_data.extend([Ct, pct])
for col, val in enumerate(row_data, 1):
c = ws3.cell(row=r_idx, column=col, value=val)
c.border = thin_border()
c.alignment = center()
# Color by drug column
if col in (1, 2):
c.fill = fill(GREY_BG)
c.font = body_font(10, bold=True, color=DARK_BLUE)
elif col in (3, 4):
pct_val = row_data[3]
if pct_val < 5:
c.fill = fill("C6EFCE") # green = eliminated
c.font = body_font(9, color=GREEN)
elif pct_val < 25:
c.fill = fill(GREEN_BG)
c.font = body_font(9)
else:
c.fill = fill(bg)
c.font = body_font(10)
elif col in (5, 6):
pct_val = row_data[5]
if pct_val < 5:
c.fill = fill("C6EFCE")
c.font = body_font(9, color=GREEN)
elif pct_val < 25:
c.fill = fill(ORANGE_BG)
c.font = body_font(9)
else:
c.fill = fill(bg)
c.font = body_font(10)
else:
pct_val = row_data[7]
if pct_val < 5:
c.fill = fill("C6EFCE")
c.font = body_font(9, color=GREEN)
elif pct_val < 25:
c.fill = fill(PURPLE_BG)
c.font = body_font(9)
else:
c.fill = fill(bg)
c.font = body_font(10)
# Format % columns nicely
if col in (4, 6, 8):
c.number_format = '0.00"%"'
# Key milestone rows — highlight "~eliminated" rows
# Add legend below table
legend_row = 5 + len(time_points) + 2
ws3.merge_cells(f"A{legend_row}:H{legend_row}")
c = ws3[f"A{legend_row}"]
c.value = "LEGEND: Green shading = < 5% remaining (drug effectively eliminated) | ~5 half-lives needed for >97% elimination"
c.font = Font(name="Arial", size=9, italic=True, color="333333")
c.fill = fill("C6EFCE")
c.alignment = left()
# ══════════════════════════════════════════════════════════════════════════════
# SHEET 4 — REAL DRUG COMPARISON TABLE
# ══════════════════════════════════════════════════════════════════════════════
ws4 = wb.create_sheet("Real Drug PK Data")
ws4.sheet_view.showGridLines = False
col_widths4 = [22, 8, 22, 24, 12, 18, 22, 30]
for col, w in enumerate(col_widths4, 1):
ws4.column_dimensions[get_column_letter(col)].width = w
ws4.row_dimensions[1].height = 36
merge_title(ws4, "A1:H1",
"REAL DRUG PHARMACOKINETIC REFERENCE TABLE",
DARK_BLUE)
ws4.row_dimensions[2].height = 8
headers4 = ["Drug", "Dose\n(mg)", "Vd\n(L/kg)", "CL\n(mL/min)", "t½", "Primary\nClearance", "Protein\nBinding (%)", "Clinical Note"]
ws4.row_dimensions[3].height = 30
for col, h in enumerate(headers4, 1):
c = ws4.cell(row=3, column=col, value=h)
c.font = hdr_font(10)
c.fill = fill(MED_BLUE)
c.alignment = center()
c.border = thin_border()
drug_data = [
# (Drug, Dose, Vd L/kg, CL mL/min, t½, Primary CL, Protein binding %, Clinical note, row_color)
("Heparin (UFH)", 5000, 0.06, "~40", "1–2 hr", "Reticuloendothelial", "High", "Stays in plasma; monitor aPTT", WHITE),
("Amoxicillin", 500, 0.47, 417, "1.2 hr", "Renal (86%)", "17–18%", "Dose-reduce in renal failure", ALT_ROW),
("Aspirin (low dose)", 325, 0.17, "575–725", "15–20 min","Hepatic (>95%)", "80–90%", "Very short t½; antithrombotic effect lasts 7–10 days (irreversible COX inhibition)", WHITE),
("Gentamicin", 80, 0.25, "50–100", "2–3 hr", "Renal (>90%)", "<10%", "Narrow TI; once-daily dosing preferred; nephrotoxic", ALT_ROW),
("Phenytoin", 300, 0.6, 167, "12–24 hr", "Hepatic (>95%)", "88–93%", "ZERO-ORDER at high doses — small dose increase causes big conc rise", WHITE),
("Carbamazepine", 400, 1.2, "50–125", "12–17 hr", "Hepatic (>95%)", "75–90%", "Induces own metabolism (autoinduction); t½ shortens with long-term use", ALT_ROW),
("Lithium", 300, 0.85, "20–40", "20–24 hr", "Renal (95–99%)", "0%", "Mirrors GFR exactly; monitor closely in renal impairment", WHITE),
("Warfarin", 5, 0.14, "~3", "36–42 hr", "Hepatic (>99%)", "99%", "Very low CL; many drug interactions via CYP2C9", ALT_ROW),
("Digoxin", 0.5, 5.0, "~130", "36–48 hr", "Renal (70–80%)", "25%", "Huge Vd → not dialysable; toxic in renal failure", WHITE),
("Diazepam", 10, 1.1, "~25", "20–100 hr","Hepatic (>99%)", "96–99%", "t½ DOUBLES in elderly due to ↑Vd (more body fat)", ALT_ROW),
("Amiodarone", 200, "60+", "~2", "40–55 DAYS","Hepatic", "96%", "Enormous Vd; t½ weeks–months; effects persist long after stopping", WHITE),
("Vancomycin", 1000, 0.7, "~65", "6–12 hr", "Renal (>90%)", "<55%", "Dose-adjust by renal function; monitor trough levels", ALT_ROW),
]
for r_idx, row in enumerate(drug_data, 4):
ws4.row_dimensions[r_idx].height = 34
*data, bg = row
for col, val in enumerate(data, 1):
c = ws4.cell(row=r_idx, column=col, value=val)
c.fill = fill(bg)
c.border = thin_border()
c.alignment = center() if col < 8 else left()
if col == 1:
c.font = Font(name="Arial", size=10, bold=True, color=DARK_BLUE)
elif col == 7:
# Highlight amiodarone t½
if "DAYS" in str(val):
c.font = Font(name="Arial", size=10, bold=True, color=RED)
else:
c.font = body_font(10)
else:
c.font = body_font(10)
# Footer note
note_row = 4 + len(drug_data) + 1
ws4.merge_cells(f"A{note_row}:H{note_row}")
c = ws4[f"A{note_row}"]
c.value = "* Values are approximate averages from published literature. TI = Therapeutic Index. CL = Clearance. Vd = Volume of Distribution."
c.font = Font(name="Arial", size=8, italic=True, color="666666")
c.alignment = left()
# ══════════════════════════════════════════════════════════════════════════════
# SHEET 5 — LOADING DOSE & STEADY STATE CALCULATOR
# ══════════════════════════════════════════════════════════════════════════════
ws5 = wb.create_sheet("Loading Dose & Steady State")
ws5.sheet_view.showGridLines = False
for col, w in zip("ABCDE", [28, 20, 24, 28, 28]):
ws5.column_dimensions[col].width = w
ws5.row_dimensions[1].height = 36
merge_title(ws5, "A1:E1",
"LOADING DOSE & STEADY-STATE CALCULATIONS",
DARK_BLUE)
ws5.row_dimensions[2].height = 8
# Section 1: Loading dose
merge_title(ws5, "A3:E3", "SECTION 1 — Loading Dose Examples", MED_BLUE, size=11)
ws5.row_dimensions[4].height = 20
for col, h in enumerate(["Drug", "Target Conc\n(mg/L)", "Vd (L/kg)", "Patient Wt\n(kg)", "Loading Dose (mg)"], 1):
c = ws5.cell(row=4, column=col, value=h)
c.font = hdr_font(10); c.fill = fill(MED_BLUE); c.alignment = center(); c.border = thin_border()
ld_examples = [
("Phenytoin", 10, 0.6, 70, 0.6*70*10),
("Phenytoin", 15, 0.6, 70, 0.6*70*15),
("Digoxin", 1.5, 5.0, 70, 5.0*70*0.0015), # target in µg/L converted
("Vancomycin", 20, 0.7, 70, 0.7*70*20),
("Lithium", 0.8, 0.85,70, 0.85*70*0.8),
]
ld_colors = [WHITE, ALT_ROW, WHITE, ALT_ROW, WHITE]
for r_idx, (row, bg) in enumerate(zip(ld_examples, ld_colors), 5):
ws5.row_dimensions[r_idx].height = 20
drug_name, target, vd_per_kg, wt, ld = row
for col, val in enumerate([drug_name, target, vd_per_kg, wt, round(ld,1)], 1):
c = ws5.cell(row=r_idx, column=col, value=val)
c.fill = fill(bg); c.border = thin_border(); c.alignment = center()
c.font = Font(name="Arial", size=10, bold=(col==5), color=(DARK_BLUE if col==5 else "000000"))
# Formula note
ws5.row_dimensions[10].height = 20
ws5.merge_cells("A10:E10")
c = ws5["A10"]
c.value = "Formula: Loading Dose = Vd (L/kg) × Weight (kg) × Target Concentration (mg/L)"
c.font = Font(name="Arial", size=10, italic=True, bold=True, color=DARK_BLUE)
c.fill = fill(LIGHT_BLUE); c.alignment = left()
ws5.row_dimensions[11].height = 12 # spacer
# Section 2: Steady State
merge_title(ws5, "A12:E12", "SECTION 2 — Time to Steady State (5 × t½ Rule)", MED_BLUE, size=11)
ws5.row_dimensions[13].height = 20
for col, h in enumerate(["Drug", "t½", "1 × t½\n(50% SS)", "3 × t½\n(87.5% SS)", "5 × t½\n(≈97% SS = Steady State)"], 1):
c = ws5.cell(row=13, column=col, value=h)
c.font = hdr_font(10); c.fill = fill(MED_BLUE); c.alignment = center(); c.border = thin_border()
ss_drugs = [
("Aspirin", "15–20 min", "15–20 min", "45–60 min", "75–100 min"),
("Amoxicillin", "1.2 hr", "1.2 hr", "3.6 hr", "6 hr"),
("Vancomycin", "6–12 hr", "6–12 hr", "18–36 hr", "1.25–2.5 days"),
("Phenytoin", "12–24 hr", "12–24 hr", "1.5–3 days", "2.5–5 days"),
("Digoxin", "36–48 hr", "36–48 hr", "4.5–6 days", "7.5–10 days"),
("Warfarin", "36–42 hr", "36–42 hr", "4.5–5.3 days","7.5–8.75 days"),
("Diazepam", "20–100 hr", "20–100 hr", "2.5–12.5 days","4–20 days"),
("Amiodarone", "40–55 days", "40–55 days", "4–6 months", "6–9 months"),
]
ss_colors = [WHITE, ALT_ROW] * 5
for r_idx, (row, bg) in enumerate(zip(ss_drugs, ss_colors), 14):
ws5.row_dimensions[r_idx].height = 20
for col, val in enumerate(row, 1):
c = ws5.cell(row=r_idx, column=col, value=val)
c.fill = fill(bg); c.border = thin_border(); c.alignment = center()
if col == 5:
c.font = Font(name="Arial", size=10, bold=True, color=DARK_BLUE)
c.fill = fill(GREEN_BG)
elif col == 1:
c.font = Font(name="Arial", size=10, bold=True, color="000000")
else:
c.font = body_font(10)
# Key rule note
last_row = 14 + len(ss_drugs) + 1
ws5.merge_cells(f"A{last_row}:E{last_row}")
c = ws5[f"A{last_row}"]
c.value = "KEY RULE: Regardless of dose size, it always takes ~5 half-lives to reach steady state — only the LEVEL (not speed) changes with dose."
c.font = Font(name="Arial", size=10, italic=True, bold=True, color=RED)
c.fill = fill(YELLOW_BG); c.alignment = left()
# ══════════════════════════════════════════════════════════════════════════════
# SAVE
# ══════════════════════════════════════════════════════════════════════════════
out_path = "/home/daytona/workspace/pharmacokinetics/Pharmacokinetics_PK_Timetable.xlsx"
wb.save(out_path)
print(f"Saved: {out_path}")