Bipap case weaning chart by respiratory therapist
BiPAP weaning protocol chart respiratory therapist criteria
https://www.journalpulmonology.org/en-how-do-i-wean-patient-…
| Parameter | Threshold to Proceed with Weaning |
|---|---|
| Underlying cause | Resolving/reversed (e.g., COPD exacerbation treated, pulmonary edema improving) |
| pH | ≥ 7.35 |
| PaCO2 | Decreased ≥10% from pre-BiPAP baseline |
| PaO2 / SpO2 | PaO2 >60 mmHg or SaO2 ≥90% on FiO2 <50% (PaO2/FiO2 >150-200) |
| Respiratory rate | <25-30 breaths/min, no accessory muscle use |
| Heart rate | 50-120 bpm |
| Systolic BP | 90-180 mmHg without vasopressors |
| Mental status | Alert, Kelly score ≤2 (follows simple commands), no sedation needed |
| Duration on BiPAP | Stable for at least the first 24 h (or per unit protocol) |
| Parameter | Pass Criteria (continue weaning) | Fail Criteria (resume BiPAP) |
|---|---|---|
| pH | ≥7.35 | <7.35 |
| PaCO2 rise | <20% above baseline | ≥20% above baseline |
| SaO2 | 88-92% on FiO2 ≤40% | <88-90% |
| RR | 8-30/min | <8 or >30/min |
| HR | 50-120 bpm | <50 or >120 bpm |
| Dyspnea (Borg scale) | ≤4 | Severe dyspnea (Borg >4) |
| Consciousness | Kelly score ≤2 | Deterioration ≥1 point |
| Day | BiPAP Schedule |
|---|---|
| Day 1 | Interrupt morning session only; continue afternoon + evening (each session ≥3 h continuous NIV, max tolerated) |
| Day 2 | Interrupt morning + afternoon sessions; continue evening/nocturnal NIV |
| Day 3 | Interrupt all daytime sessions; nocturnal BiPAP only |
| Day 4+ | Discontinue entirely if stable overnight; step down to oxygen therapy or room air |
| Frequency | Parameters to Chart |
|---|---|
| Every 15 min x first hour off BiPAP | SpO2, RR, HR, work of breathing/accessory muscle use |
| Hourly thereafter | ABG (1 h post-change, then per condition), mental status, synchrony |
| Each session | Tolerance, mask fit/leak, secretions, chest wall movement |
Daily Screen (stable >6h, RR<24, HR<110, pH normal, SpO2 >88% on 4L O2 while on BiPAP)
|
PASS? --- NO --> Continue BiPAP, monitor, rescreen next shift
|
YES
|
Trial off BiPAP (1-4h) on nasal/Venturi O2, target SpO2
|
Tolerated? --- NO --> Resume BiPAP, reassess cause of failure
|
YES
|
Extend off-periods per gradual schedule (Section 3) --> daytime off, nocturnal only --> full discontinuation
Generate an Excel tracking sheet for daily BiPAP weaning logs
mkdir -p /tmp/workspace/bipap-weaning && ls -la /tmp/workspace/
~/bipap-weaning/build_workbook.py
"""
Build a Daily BiPAP Weaning Tracking Log Excel workbook for Respiratory Therapists.
Sheets:
1. Daily Weaning Log - main shift-by-shift entry log
2. Gradual Interruption Schedule - day-by-day session tracker
3. Readiness & Failure Criteria (Reference) - static clinical reference table
4. Instructions & Legend
"""
import openpyxl
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.worksheet.datavalidation import DataValidation
from openpyxl.utils import get_column_letter
from openpyxl.formatting.rule import CellIsRule, FormulaRule
FONT_NAME = "Arial"
# ---- Style constants ----
TITLE_FONT = Font(name=FONT_NAME, size=14, bold=True, color="FFFFFF")
HEADER_FONT = Font(name=FONT_NAME, size=10, bold=True, color="FFFFFF")
SUBHEADER_FONT = Font(name=FONT_NAME, size=10, bold=True, color="000000")
BODY_FONT = Font(name=FONT_NAME, size=10)
NOTE_FONT = Font(name=FONT_NAME, size=9, italic=True, color="595959")
HEADER_FILL = PatternFill("solid", fgColor="1F4E78")
SECTION_FILL = PatternFill("solid", fgColor="2E75B6")
ALT_FILL = PatternFill("solid", fgColor="D9E1F2")
PASS_FILL = PatternFill("solid", fgColor="C6EFCE")
FAIL_FILL = PatternFill("solid", fgColor="FFC7CE")
INPUT_FILL = PatternFill("solid", fgColor="FFF2CC")
THIN = Side(style="thin", color="B7B7B7")
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True)
LEFT = Alignment(horizontal="left", vertical="center", wrap_text=True)
wb = Workbook()
# =========================================================
# SHEET 1: Daily Weaning Log
# =========================================================
ws = wb.active
ws.title = "Daily Weaning Log"
ws.sheet_view.showGridLines = False
ws.merge_cells("A1:V1")
ws["A1"] = "BiPAP / NIV DAILY WEANING TRACKING LOG"
ws["A1"].font = TITLE_FONT
ws["A1"].fill = HEADER_FILL
ws["A1"].alignment = CENTER
ws.row_dimensions[1].height = 26
ws.merge_cells("A2:V2")
ws["A2"] = "Patient Name: _____________________ MRN: _____________ Unit/Bed: _______ Admit Dx: ________________________"
ws["A2"].font = Font(name=FONT_NAME, size=10, bold=True)
ws["A2"].alignment = LEFT
ws.row_dimensions[2].height = 20
headers = [
"Date", "Shift\n(Day/Eve/Night)", "Time", "RT Initials",
"IPAP\n(cmH2O)", "EPAP\n(cmH2O)", "FiO2\n(%)",
"RR\n(/min)", "HR\n(bpm)", "SpO2\n(%)", "Systolic BP\n(mmHg)",
"ABG pH", "ABG PaCO2\n(mmHg)", "ABG PaO2\n(mmHg)",
"Kelly Score\n(0-2 pass)", "Borg Dyspnea\n(0-10)",
"Readiness\nScreen (Pass/Fail)", "Trial Off Duration\n(hrs)",
"Trial Outcome\n(Tolerated/Failed)", "Failure Criteria Met\n(list or None)",
"Plan / Next Step", "RT Signature"
]
ws.append([""] * len(headers)) # placeholder row 3 will be header actually row 3
header_row = 3
for c, h in enumerate(headers, start=1):
cell = ws.cell(row=header_row, column=c, value=h)
cell.font = HEADER_FONT
cell.fill = HEADER_FILL
cell.alignment = CENTER
cell.border = BORDER
ws.row_dimensions[header_row].height = 42
col_widths = [11, 12, 8, 10, 9, 9, 8, 8, 8, 8, 11, 9, 11, 11, 11, 11, 13, 13, 14, 20, 26, 14]
for i, w in enumerate(col_widths, start=1):
ws.column_dimensions[get_column_letter(i)].width = w
DATA_START = header_row + 1
DATA_END = DATA_START + 39 # 40 blank rows for logging (roughly a month of shifts)
for r in range(DATA_START, DATA_END + 1):
for c in range(1, len(headers) + 1):
cell = ws.cell(row=r, column=c)
cell.border = BORDER
cell.font = BODY_FONT
cell.alignment = CENTER
if (r - DATA_START) % 2 == 1:
for c in range(1, len(headers) + 1):
ws.cell(row=r, column=c).fill = ALT_FILL
# Data validations
dv_shift = DataValidation(type="list", formula1='"Day,Evening,Night"', allow_blank=True)
dv_screen = DataValidation(type="list", formula1='"Pass,Fail"', allow_blank=True)
dv_outcome = DataValidation(type="list", formula1='"Tolerated,Failed,N/A - Continuous BiPAP"', allow_blank=True)
dv_kelly = DataValidation(type="list", formula1='"0,1,2,3,4,5"', allow_blank=True)
for dv in (dv_shift, dv_screen, dv_outcome, dv_kelly):
ws.add_data_validation(dv)
dv_shift.add(f"B{DATA_START}:B{DATA_END}")
dv_screen.add(f"Q{DATA_START}:Q{DATA_END}")
dv_outcome.add(f"S{DATA_START}:S{DATA_END}")
dv_kelly.add(f"O{DATA_START}:O{DATA_END}")
# Conditional formatting - Pass/Fail highlighting for readiness screen & outcome
ws.conditional_formatting.add(
f"Q{DATA_START}:Q{DATA_END}",
CellIsRule(operator="equal", formula=['"Pass"'], fill=PASS_FILL)
)
ws.conditional_formatting.add(
f"Q{DATA_START}:Q{DATA_END}",
CellIsRule(operator="equal", formula=['"Fail"'], fill=FAIL_FILL)
)
ws.conditional_formatting.add(
f"S{DATA_START}:S{DATA_END}",
CellIsRule(operator="equal", formula=['"Tolerated"'], fill=PASS_FILL)
)
ws.conditional_formatting.add(
f"S{DATA_START}:S{DATA_END}",
CellIsRule(operator="equal", formula=['"Failed"'], fill=FAIL_FILL)
)
# Highlight abnormal vitals automatically (RR col H, HR col I, SpO2 col J, pH col L)
ws.conditional_formatting.add(
f"H{DATA_START}:H{DATA_END}",
FormulaRule(formula=[f"OR(H{DATA_START}<8,H{DATA_START}>30)"], fill=FAIL_FILL)
)
ws.conditional_formatting.add(
f"I{DATA_START}:I{DATA_END}",
FormulaRule(formula=[f"OR(I{DATA_START}<50,I{DATA_START}>120)"], fill=FAIL_FILL)
)
ws.conditional_formatting.add(
f"J{DATA_START}:J{DATA_END}",
FormulaRule(formula=[f"J{DATA_START}<88"], fill=FAIL_FILL)
)
ws.conditional_formatting.add(
f"L{DATA_START}:L{DATA_END}",
FormulaRule(formula=[f"L{DATA_START}<7.35"], fill=FAIL_FILL)
)
ws.freeze_panes = f"A{DATA_START}"
note_row = DATA_END + 2
ws.merge_cells(f"A{note_row}:V{note_row}")
ws[f"A{note_row}"] = ("Legend: Readiness Screen = daily bedside criteria per BTS/ICS guideline (see 'Readiness & Failure Criteria' tab). "
"Red fill = value outside safe threshold - reassess and notify physician.")
ws[f"A{note_row}"].font = NOTE_FONT
ws[f"A{note_row}"].alignment = LEFT
# =========================================================
# SHEET 2: Gradual Interruption Schedule
# =========================================================
ws2 = wb.create_sheet("Gradual Interruption Schedule")
ws2.sheet_view.showGridLines = False
ws2.merge_cells("A1:K1")
ws2["A1"] = "BiPAP GRADUAL INTERRUPTION / SESSION TRACKER"
ws2["A1"].font = TITLE_FONT
ws2["A1"].fill = HEADER_FILL
ws2["A1"].alignment = CENTER
ws2.row_dimensions[1].height = 24
ws2.merge_cells("A2:K2")
ws2["A2"] = "Patient Name: _____________________ MRN: _____________ Weaning Start Date: ___________"
ws2["A2"].font = Font(name=FONT_NAME, size=10, bold=True)
ws2.row_dimensions[2].height = 20
headers2 = [
"Weaning Day", "Date", "Morning Session\nOff? (Y/N)", "Morning Off Duration (hrs)",
"Afternoon Session\nOff? (Y/N)", "Afternoon Off Duration (hrs)",
"Evening/Night Session\nOff? (Y/N)", "Night Off Duration (hrs)",
"SpO2 Range During Off-Periods (%)", "Tolerated Overall (Y/N)", "RT Notes"
]
hr2 = 3
for c, h in enumerate(headers2, start=1):
cell = ws2.cell(row=hr2, column=c, value=h)
cell.font = HEADER_FONT
cell.fill = SECTION_FILL
cell.alignment = CENTER
cell.border = BORDER
ws2.row_dimensions[hr2].height = 40
widths2 = [11, 12, 14, 16, 14, 16, 16, 14, 20, 16, 30]
for i, w in enumerate(widths2, start=1):
ws2.column_dimensions[get_column_letter(i)].width = w
start2 = hr2 + 1
end2 = start2 + 9 # Day 1-10
for i, r in enumerate(range(start2, end2 + 1), start=1):
ws2.cell(row=r, column=1, value=f"Day {i}").font = SUBHEADER_FONT
ws2.cell(row=r, column=1).alignment = CENTER
for c in range(1, len(headers2) + 1):
cell = ws2.cell(row=r, column=c)
cell.border = BORDER
cell.font = BODY_FONT
cell.alignment = CENTER
if i % 2 == 0:
for c in range(2, len(headers2) + 1):
ws2.cell(row=r, column=c).fill = ALT_FILL
dv_yn = DataValidation(type="list", formula1='"Y,N"', allow_blank=True)
ws2.add_data_validation(dv_yn)
for col in ["C", "E", "G", "J"]:
dv_yn.add(f"{col}{start2}:{col}{end2}")
ws2.conditional_formatting.add(f"J{start2}:J{end2}", CellIsRule(operator="equal", formula=['"Y"'], fill=PASS_FILL))
ws2.conditional_formatting.add(f"J{start2}:J{end2}", CellIsRule(operator="equal", formula=['"N"'], fill=FAIL_FILL))
note2 = end2 + 2
ws2.merge_cells(f"A{note2}:K{note2}")
ws2[f"A{note2}"] = ("Reference schedule (Faverio et al. protocol): Day1 = interrupt AM session; Day2 = interrupt AM+PM; "
"Day3 = interrupt all daytime sessions (nocturnal BiPAP only); Day4+ = discontinue if stable overnight. "
"Each on-BiPAP session should be >=3 hours or maximum tolerated; off-periods use O2 via nasal cannula/Venturi titrated to SpO2 88-92%.")
ws2[f"A{note2}"].font = NOTE_FONT
ws2[f"A{note2}"].alignment = LEFT
ws2.merge_cells(f"A{note2+1}:K{note2+1}")
ws2[f"A{note2+1}"] = "Source: Faverio P, et al., summarized in Karim & Ashkenazi PMC12295356; Duan J, et al. protocol-directed NIV weaning."
ws2[f"A{note2+1}"].font = NOTE_FONT
# =========================================================
# SHEET 3: Readiness & Failure Criteria (Reference)
# =========================================================
ws3 = wb.create_sheet("Readiness & Failure Criteria")
ws3.sheet_view.showGridLines = False
ws3.merge_cells("A1:D1")
ws3["A1"] = "REFERENCE: BiPAP/NIV WEANING CLINICAL CRITERIA"
ws3["A1"].font = TITLE_FONT
ws3["A1"].fill = HEADER_FILL
ws3["A1"].alignment = CENTER
ws3.row_dimensions[1].height = 24
def add_section(ws, start_row, title, rows, col_widths=(28, 34, 34, 0)):
r = start_row
ws.merge_cells(f"A{r}:C{r}")
ws[f"A{r}"] = title
ws[f"A{r}"].font = HEADER_FONT
ws[f"A{r}"].fill = SECTION_FILL
ws[f"A{r}"].alignment = CENTER
r += 1
for row_vals in rows:
for c, val in enumerate(row_vals, start=1):
cell = ws.cell(row=r, column=c, value=val)
cell.font = BODY_FONT
cell.border = BORDER
cell.alignment = LEFT if c > 1 else LEFT
r += 1
return r + 1
row_ptr = 3
row_ptr = add_section(ws3, row_ptr, "1. DAILY READINESS SCREEN (Baseline on BiPAP) - Must meet ALL to proceed", [
("Parameter", "Threshold", ""),
("pH", "≥ 7.35", ""),
("PaCO2", "Decreased ≥10% from pre-BiPAP baseline", ""),
("PaO2 / SaO2", "PaO2 >60 mmHg or SaO2 ≥90% on FiO2 <50%", ""),
("Respiratory rate", "<25-30 /min, no accessory muscle use", ""),
("Heart rate", "50-120 bpm", ""),
("Systolic BP", "90-180 mmHg without vasopressors", ""),
("Mental status", "Alert, Kelly score ≤2, no sedation", ""),
("Stability on BiPAP", "Clinically stable ≥6-24 h", ""),
])
row_ptr = add_section(ws3, row_ptr, "2. SPONTANEOUS TRIAL OFF BiPAP (1-4 hrs on O2 via NC/Venturi) - Pass vs Fail", [
("Parameter", "Pass (continue weaning)", "Fail (resume BiPAP)"),
("pH", "≥ 7.35", "< 7.35"),
("PaCO2 rise", "< 20% above baseline", "≥ 20% above baseline"),
("SaO2", "88-92% on FiO2 ≤40%", "< 88-90%"),
("Respiratory rate", "8-30 /min", "< 8 or > 30 /min"),
("Heart rate", "50-120 bpm", "< 50 or > 120 bpm"),
("Dyspnea (Borg scale)", "≤ 4", "> 4 (severe)"),
("Consciousness", "Kelly score ≤2, stable", "Kelly score worsens ≥1 point"),
])
row_ptr = add_section(ws3, row_ptr, "3. MAJOR CRITERIA - REINSTITUTE BiPAP IMMEDIATELY (any one)", [
("Respiratory rate", "<8 or >30 /min", ""),
("Systolic BP", "<90 or >180 mmHg without vasopressors", ""),
("Heart rate", "<50 or >120 bpm", ""),
("Kelly score", ">2", ""),
("SaO2", "<90% on FiO2 ≥40%", ""),
("pH", "<7.35", ""),
("PaCO2", ">20% increase from weaning start", ""),
("Dyspnea", "Severe (Borg >4)", ""),
])
row_ptr = add_section(ws3, row_ptr, "4. MINOR / GRADUAL FAILURE INDICATORS - Reassess closely", [
("PaO2/FiO2 ratio", "Reduction ≥30% from baseline", ""),
("PaCO2", "Increase ≥20%", ""),
("Alertness", "Worsening on Kelly scale (≥1 point)", ""),
("Respiratory distress", "New onset or persistent", ""),
("Fatigue", "Exhaustion", ""),
("Secretions", "Accumulation despite physio/cough assist", ""),
])
row_ptr = add_section(ws3, row_ptr, "5. EMERGENCY / INTUBATE - DO NOT ATTEMPT FURTHER WEANING", [
("Respiratory arrest", "", ""),
("Respiratory pauses with unconsciousness", "", ""),
("Severe hemodynamic instability", "", ""),
("Intolerance forcing device discontinuation", "", ""),
])
ws3.column_dimensions["A"].width = 30
ws3.column_dimensions["B"].width = 38
ws3.column_dimensions["C"].width = 38
src_row = row_ptr
ws3.merge_cells(f"A{src_row}:C{src_row}")
ws3[f"A{src_row}"] = ("Sources: BTS/ICS 2016 NIV Guideline (via Pulmonology journal review); Duan J et al. protocol-directed NIV weaning; "
"Faverio P et al. NIV weaning algorithm; Washington Manual of Medical Therapeutics.")
ws3[f"A{src_row}"].font = NOTE_FONT
ws3[f"A{src_row}"].alignment = LEFT
# =========================================================
# SHEET 4: Instructions & Legend
# =========================================================
ws4 = wb.create_sheet("Instructions")
ws4.sheet_view.showGridLines = False
ws4.merge_cells("A1:B1")
ws4["A1"] = "HOW TO USE THIS TRACKING SHEET"
ws4["A1"].font = TITLE_FONT
ws4["A1"].fill = HEADER_FILL
ws4["A1"].alignment = CENTER
ws4.row_dimensions[1].height = 24
ws4.column_dimensions["A"].width = 26
ws4.column_dimensions["B"].width = 90
instructions = [
("Daily Weaning Log", "Complete one row per shift/assessment. Enter vitals, ABG, screen result (Pass/Fail), and trial outcome. "
"Cells auto-highlight red if a value crosses an unsafe threshold (e.g., pH <7.35, SpO2 <88%)."),
("Gradual Interruption Schedule", "Use once the patient passes the daily readiness screen. Track each on/off BiPAP session per the "
"gradual interruption protocol (AM -> AM+PM -> all daytime -> nocturnal only -> full discontinuation)."),
("Readiness & Failure Criteria", "Static clinical reference - keep visible while completing the log. Do not edit; use as the source of truth "
"for Pass/Fail and Major/Minor criteria decisions."),
("Color Key", "Green fill = criterion met / tolerated. Red fill = criterion failed / unsafe value - notify physician and resume BiPAP. "
"Yellow = fields intended for manual entry."),
("Data Validation", "Dropdown lists are built into Shift, Readiness Screen, Trial Outcome, Kelly Score, and Y/N columns to standardize entries."),
("Disclaimer", "This tool is a documentation aid based on published NIV weaning protocols. It does not replace institutional policy, "
"physician orders, or clinical judgment. Always follow your unit's approved NIV/BiPAP weaning protocol."),
]
r = 3
for label, text in instructions:
ws4.cell(row=r, column=1, value=label).font = SUBHEADER_FONT
ws4.cell(row=r, column=1).alignment = Alignment(vertical="top", wrap_text=True)
ws4.cell(row=r, column=1).border = BORDER
cell = ws4.cell(row=r, column=2, value=text)
cell.font = BODY_FONT
cell.alignment = Alignment(vertical="top", wrap_text=True)
cell.border = BORDER
ws4.row_dimensions[r].height = 55
r += 1
out_path = "/tmp/workspace/bipap-weaning/BiPAP_Weaning_Tracking_Log.xlsx"
wb.save(out_path)
print("Saved:", out_path)
cd /tmp/workspace/bipap-weaning && python3 build_workbook.py
cd /tmp/workspace/bipap-weaning && python3 -c " import openpyxl wb = openpyxl.load_workbook('BiPAP_Weaning_Tracking_Log.xlsx') print(wb.sheetnames) for s in wb.sheetnames: ws = wb[s] print(s, ws.dimensions) " ls -la /tmp/workspace/bipap-weaning/
BiPAP Weaning Tracking Log
Spreadsheet · XLSX
All
~/bipap-weaning/build_workbook.py
"""
Build a Daily BiPAP Weaning Tracking Log Excel workbook for Respiratory Therapists.
Sheets:
1. Detailed Shift Log - one row per shift/assessment (extended duration, ~40 rows), dual units (mmHg + kPa)
2. 7-Day EMR Summary - one row per calendar day, EMR flowsheet style, condensed AM/PM/Night blocks
3. Gradual Interruption Schedule - day-by-day on/off session tracker
4. Readiness & Failure Criteria (Reference) - static clinical reference table, dual units
5. Instructions & Legend
"""
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.worksheet.datavalidation import DataValidation
from openpyxl.utils import get_column_letter
from openpyxl.formatting.rule import CellIsRule, FormulaRule
FONT_NAME = "Arial"
MMHG_TO_KPA = 0.133322
# ---- Style constants ----
TITLE_FONT = Font(name=FONT_NAME, size=14, bold=True, color="FFFFFF")
HEADER_FONT = Font(name=FONT_NAME, size=10, bold=True, color="FFFFFF")
SUBHEADER_FONT = Font(name=FONT_NAME, size=10, bold=True, color="000000")
BODY_FONT = Font(name=FONT_NAME, size=10)
CALC_FONT = Font(name=FONT_NAME, size=10, color="000000", italic=True) # computed / converted cells
NOTE_FONT = Font(name=FONT_NAME, size=9, italic=True, color="595959")
HEADER_FILL = PatternFill("solid", fgColor="1F4E78")
SECTION_FILL = PatternFill("solid", fgColor="2E75B6")
GROUP_FILL = PatternFill("solid", fgColor="8EA9DB")
ALT_FILL = PatternFill("solid", fgColor="D9E1F2")
PASS_FILL = PatternFill("solid", fgColor="C6EFCE")
FAIL_FILL = PatternFill("solid", fgColor="FFC7CE")
CALC_FILL = PatternFill("solid", fgColor="F2F2F2")
THIN = Side(style="thin", color="B7B7B7")
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True)
LEFT = Alignment(horizontal="left", vertical="center", wrap_text=True)
def style_title(ws, cell_range, text):
ws.merge_cells(cell_range)
top_left = cell_range.split(":")[0]
ws[top_left] = text
ws[top_left].font = TITLE_FONT
ws[top_left].fill = HEADER_FILL
ws[top_left].alignment = CENTER
wb = Workbook()
# =========================================================
# SHEET 1: Detailed Shift Log (extended duration, dual units)
# =========================================================
ws = wb.active
ws.title = "Detailed Shift Log"
ws.sheet_view.showGridLines = False
style_title(ws, "A1:X1", "BiPAP / NIV DETAILED SHIFT-BY-SHIFT WEANING LOG")
ws.row_dimensions[1].height = 26
ws.merge_cells("A2:X2")
ws["A2"] = "Patient Name: _____________________ MRN: _____________ Unit/Bed: _______ Admit Dx: ________________________"
ws["A2"].font = Font(name=FONT_NAME, size=10, bold=True)
ws["A2"].alignment = LEFT
ws.row_dimensions[2].height = 20
headers = [
"Date", "Shift\n(Day/Eve/Night)", "Time", "RT Initials",
"IPAP\n(cmH2O)", "EPAP\n(cmH2O)", "FiO2\n(%)",
"RR\n(/min)", "HR\n(bpm)", "SpO2\n(%)", "Systolic BP\n(mmHg)",
"ABG pH",
"PaCO2\n(mmHg)", "PaCO2\n(kPa, auto)",
"PaO2\n(mmHg)", "PaO2\n(kPa, auto)",
"Kelly Score\n(0-2 pass)", "Borg Dyspnea\n(0-10)",
"Readiness\nScreen (Pass/Fail)", "Trial Off Duration\n(hrs)",
"Trial Outcome\n(Tolerated/Failed)", "Failure Criteria Met\n(list or None)",
"Plan / Next Step", "RT Signature"
]
header_row = 3
for c, h in enumerate(headers, start=1):
cell = ws.cell(row=header_row, column=c, value=h)
cell.font = HEADER_FONT
cell.fill = HEADER_FILL
cell.alignment = CENTER
cell.border = BORDER
ws.row_dimensions[header_row].height = 42
col_widths = [11, 12, 8, 10, 9, 9, 8, 8, 8, 8, 11, 9, 10, 11, 10, 11, 11, 11, 13, 13, 14, 20, 26, 14]
for i, w in enumerate(col_widths, start=1):
ws.column_dimensions[get_column_letter(i)].width = w
DATA_START = header_row + 1
DATA_END = DATA_START + 39 # 40 blank rows for logging (roughly a month of shifts)
KPA_CO2_COL = 14 # column N
KPA_O2_COL = 16 # column P
MMHG_CO2_COL = 13
MMHG_O2_COL = 15
for r in range(DATA_START, DATA_END + 1):
for c in range(1, len(headers) + 1):
cell = ws.cell(row=r, column=c)
cell.border = BORDER
cell.font = BODY_FONT
cell.alignment = CENTER
if (r - DATA_START) % 2 == 1:
for c in range(1, len(headers) + 1):
ws.cell(row=r, column=c).fill = ALT_FILL
# Auto-convert mmHg -> kPa (computed, non-editable style)
co2_mm = f"{get_column_letter(MMHG_CO2_COL)}{r}"
o2_mm = f"{get_column_letter(MMHG_O2_COL)}{r}"
ws.cell(row=r, column=KPA_CO2_COL, value=f'=IF({co2_mm}="","",ROUND({co2_mm}*{MMHG_TO_KPA},2))')
ws.cell(row=r, column=KPA_O2_COL, value=f'=IF({o2_mm}="","",ROUND({o2_mm}*{MMHG_TO_KPA},2))')
ws.cell(row=r, column=KPA_CO2_COL).font = CALC_FONT
ws.cell(row=r, column=KPA_O2_COL).font = CALC_FONT
ws.cell(row=r, column=KPA_CO2_COL).fill = CALC_FILL
ws.cell(row=r, column=KPA_O2_COL).fill = CALC_FILL
# Data validations
dv_shift = DataValidation(type="list", formula1='"Day,Evening,Night"', allow_blank=True)
dv_screen = DataValidation(type="list", formula1='"Pass,Fail"', allow_blank=True)
dv_outcome = DataValidation(type="list", formula1='"Tolerated,Failed,N/A - Continuous BiPAP"', allow_blank=True)
dv_kelly = DataValidation(type="list", formula1='"0,1,2,3,4,5"', allow_blank=True)
for dv in (dv_shift, dv_screen, dv_outcome, dv_kelly):
ws.add_data_validation(dv)
dv_shift.add(f"B{DATA_START}:B{DATA_END}")
dv_screen.add(f"S{DATA_START}:S{DATA_END}")
dv_outcome.add(f"U{DATA_START}:U{DATA_END}")
dv_kelly.add(f"Q{DATA_START}:Q{DATA_END}")
# Conditional formatting
ws.conditional_formatting.add(f"S{DATA_START}:S{DATA_END}", CellIsRule(operator="equal", formula=['"Pass"'], fill=PASS_FILL))
ws.conditional_formatting.add(f"S{DATA_START}:S{DATA_END}", CellIsRule(operator="equal", formula=['"Fail"'], fill=FAIL_FILL))
ws.conditional_formatting.add(f"U{DATA_START}:U{DATA_END}", CellIsRule(operator="equal", formula=['"Tolerated"'], fill=PASS_FILL))
ws.conditional_formatting.add(f"U{DATA_START}:U{DATA_END}", CellIsRule(operator="equal", formula=['"Failed"'], fill=FAIL_FILL))
ws.conditional_formatting.add(f"H{DATA_START}:H{DATA_END}", FormulaRule(formula=[f"OR(H{DATA_START}<8,H{DATA_START}>30)"], fill=FAIL_FILL))
ws.conditional_formatting.add(f"I{DATA_START}:I{DATA_END}", FormulaRule(formula=[f"OR(I{DATA_START}<50,I{DATA_START}>120)"], fill=FAIL_FILL))
ws.conditional_formatting.add(f"J{DATA_START}:J{DATA_END}", FormulaRule(formula=[f"J{DATA_START}<88"], fill=FAIL_FILL))
ws.conditional_formatting.add(f"L{DATA_START}:L{DATA_END}", FormulaRule(formula=[f"L{DATA_START}<7.35"], fill=FAIL_FILL))
ws.freeze_panes = f"A{DATA_START}"
note_row = DATA_END + 2
ws.merge_cells(f"A{note_row}:X{note_row}")
ws[f"A{note_row}"] = ("Legend: Enter PaCO2/PaO2 in mmHg - kPa columns auto-calculate (grey italic, formula-driven, do not overwrite). "
"Readiness Screen = daily bedside criteria per BTS/ICS guideline (see 'Readiness & Failure Criteria' tab). "
"Red fill = value outside safe threshold - reassess and notify physician.")
ws[f"A{note_row}"].font = NOTE_FONT
ws[f"A{note_row}"].alignment = LEFT
# =========================================================
# SHEET 2: 7-Day EMR Summary (single row per day, condensed flowsheet style)
# =========================================================
ws2 = wb.create_sheet("7-Day EMR Summary")
ws2.sheet_view.showGridLines = False
style_title(ws2, "A1:S1", "BiPAP WEANING - 7-DAY EMR-STYLE SUMMARY FLOWSHEET (One Row per Day)")
ws2.row_dimensions[1].height = 24
ws2.merge_cells("A2:S2")
ws2["A2"] = "Patient Name: _____________________ MRN: _____________ Weaning Start Date: ___________"
ws2["A2"].font = Font(name=FONT_NAME, size=10, bold=True)
ws2.row_dimensions[2].height = 20
# Group header row (row 3) + sub-header row (row 4)
group_row, sub_row = 3, 4
groups = [
("Date /\nWeaning Day", 2, ["", ""]), # cols A-B (merged vertically via same content) -> handle separately
]
# Build columns explicitly for clarity
col_defs = [
# (group label or None, sub label, width)
(None, "Date", 11),
(None, "Weaning\nDay #", 8),
("AM SHIFT", "Settings\n(IPAP/EPAP/FiO2)", 16),
("AM SHIFT", "Vitals\n(RR/HR/SpO2)", 14),
("AM SHIFT", "Hrs Off\nBiPAP", 8),
("PM SHIFT", "Settings\n(IPAP/EPAP/FiO2)", 16),
("PM SHIFT", "Vitals\n(RR/HR/SpO2)", 14),
("PM SHIFT", "Hrs Off\nBiPAP", 8),
("NIGHT SHIFT", "Settings\n(IPAP/EPAP/FiO2)", 16),
("NIGHT SHIFT", "Vitals\n(RR/HR/SpO2)", 14),
("NIGHT SHIFT", "Hrs Off\nBiPAP", 8),
("DAILY ABG", "pH", 8),
("DAILY ABG", "PaCO2\n(mmHg)", 9),
("DAILY ABG", "PaCO2\n(kPa, auto)", 10),
("DAILY ABG", "PaO2\n(mmHg)", 9),
("DAILY ABG", "PaO2\n(kPa, auto)", 10),
(None, "Readiness\nScreen (P/F)", 12),
(None, "Total Hrs\nOff /24h", 9),
(None, "Overall\nOutcome", 13),
(None, "Plan / RT Sign-off", 26),
]
n_cols = len(col_defs)
for i, (grp, sub, width) in enumerate(col_defs, start=1):
ws2.column_dimensions[get_column_letter(i)].width = width
# Write group header row with merges for contiguous same-group cells
i = 1
while i <= n_cols:
grp = col_defs[i - 1][0]
if grp is None:
cell = ws2.cell(row=group_row, column=i, value="")
cell.fill = HEADER_FILL
cell.border = BORDER
i += 1
continue
j = i
while j <= n_cols and col_defs[j - 1][0] == grp:
j += 1
if j - 1 > i:
ws2.merge_cells(start_row=group_row, start_column=i, end_row=group_row, end_column=j - 1)
cell = ws2.cell(row=group_row, column=i, value=grp)
cell.font = HEADER_FONT
cell.fill = SECTION_FILL if "SHIFT" not in grp else GROUP_FILL
cell.alignment = CENTER
cell.border = BORDER
for cc in range(i, j):
ws2.cell(row=group_row, column=cc).border = BORDER
ws2.cell(row=group_row, column=cc).fill = SECTION_FILL if "SHIFT" not in grp else GROUP_FILL
i = j
ws2.row_dimensions[group_row].height = 18
for c, (grp, sub, width) in enumerate(col_defs, start=1):
cell = ws2.cell(row=sub_row, column=c, value=sub)
cell.font = HEADER_FONT
cell.fill = HEADER_FILL
cell.alignment = CENTER
cell.border = BORDER
ws2.row_dimensions[sub_row].height = 36
start3 = sub_row + 1
end3 = start3 + 6 # 7 days
DATE_COL, DAY_COL = 1, 2
ABG_PH_COL = 12
ABG_CO2_MM_COL = 13
ABG_CO2_KPA_COL = 14
ABG_O2_MM_COL = 15
ABG_O2_KPA_COL = 16
SCREEN_COL = 17
TOTALHRS_COL = 18
OUTCOME_COL = 19
PLAN_COL = 20
for idx, r in enumerate(range(start3, end3 + 1), start=1):
ws2.cell(row=r, column=DAY_COL, value=f"Day {idx}").font = SUBHEADER_FONT
for c in range(1, n_cols + 1):
cell = ws2.cell(row=r, column=c)
cell.border = BORDER
cell.font = BODY_FONT
cell.alignment = CENTER
if idx % 2 == 0:
for c in range(1, n_cols + 1):
ws2.cell(row=r, column=c).fill = ALT_FILL
co2_mm = f"{get_column_letter(ABG_CO2_MM_COL)}{r}"
o2_mm = f"{get_column_letter(ABG_O2_MM_COL)}{r}"
ws2.cell(row=r, column=ABG_CO2_KPA_COL, value=f'=IF({co2_mm}="","",ROUND({co2_mm}*{MMHG_TO_KPA},2))')
ws2.cell(row=r, column=ABG_O2_KPA_COL, value=f'=IF({o2_mm}="","",ROUND({o2_mm}*{MMHG_TO_KPA},2))')
ws2.cell(row=r, column=ABG_CO2_KPA_COL).font = CALC_FONT
ws2.cell(row=r, column=ABG_O2_KPA_COL).font = CALC_FONT
ws2.cell(row=r, column=ABG_CO2_KPA_COL).fill = CALC_FILL
ws2.cell(row=r, column=ABG_O2_KPA_COL).fill = CALC_FILL
dv_screen2 = DataValidation(type="list", formula1='"Pass,Fail"', allow_blank=True)
dv_outcome2 = DataValidation(type="list", formula1='"Fully Weaned,Partial - Nocturnal Only,Not Tolerated - Resumed BiPAP"', allow_blank=True)
ws2.add_data_validation(dv_screen2)
ws2.add_data_validation(dv_outcome2)
dv_screen2.add(f"{get_column_letter(SCREEN_COL)}{start3}:{get_column_letter(SCREEN_COL)}{end3}")
dv_outcome2.add(f"{get_column_letter(OUTCOME_COL)}{start3}:{get_column_letter(OUTCOME_COL)}{end3}")
sc = get_column_letter(SCREEN_COL)
oc = get_column_letter(OUTCOME_COL)
ws2.conditional_formatting.add(f"{sc}{start3}:{sc}{end3}", CellIsRule(operator="equal", formula=['"Pass"'], fill=PASS_FILL))
ws2.conditional_formatting.add(f"{sc}{start3}:{sc}{end3}", CellIsRule(operator="equal", formula=['"Fail"'], fill=FAIL_FILL))
ws2.conditional_formatting.add(f"{oc}{start3}:{oc}{end3}", CellIsRule(operator="equal", formula=['"Fully Weaned"'], fill=PASS_FILL))
ws2.conditional_formatting.add(f"{oc}{start3}:{oc}{end3}", CellIsRule(operator="equal", formula=['"Not Tolerated - Resumed BiPAP"'], fill=FAIL_FILL))
ph_c = get_column_letter(ABG_PH_COL)
ws2.conditional_formatting.add(f"{ph_c}{start3}:{ph_c}{end3}", FormulaRule(formula=[f"{ph_c}{start3}<7.35"], fill=FAIL_FILL))
ws2.freeze_panes = f"A{start3}"
note3 = end3 + 2
ws2.merge_cells(f"A{note3}:{get_column_letter(n_cols)}{note3}")
ws2[f"A{note3}"] = ("EMR-style condensed flowsheet: one row = one calendar day. Combine each shift's settings/vitals into the single cell "
"(e.g., '10/8, RR18 HR88 SpO2 94'). Designed for quick 7-day-at-a-glance review or copy/paste into EMR notes. "
"For full granular per-shift documentation use the 'Detailed Shift Log' tab instead.")
ws2[f"A{note3}"].font = NOTE_FONT
ws2[f"A{note3}"].alignment = LEFT
# =========================================================
# SHEET 3: Gradual Interruption Schedule
# =========================================================
ws3 = wb.create_sheet("Gradual Interruption Schedule")
ws3.sheet_view.showGridLines = False
style_title(ws3, "A1:K1", "BiPAP GRADUAL INTERRUPTION / SESSION TRACKER")
ws3.row_dimensions[1].height = 24
ws3.merge_cells("A2:K2")
ws3["A2"] = "Patient Name: _____________________ MRN: _____________ Weaning Start Date: ___________"
ws3["A2"].font = Font(name=FONT_NAME, size=10, bold=True)
ws3.row_dimensions[2].height = 20
headers3 = [
"Weaning Day", "Date", "Morning Session\nOff? (Y/N)", "Morning Off Duration (hrs)",
"Afternoon Session\nOff? (Y/N)", "Afternoon Off Duration (hrs)",
"Evening/Night Session\nOff? (Y/N)", "Night Off Duration (hrs)",
"SpO2 Range During Off-Periods (%)", "Tolerated Overall (Y/N)", "RT Notes"
]
hr3 = 3
for c, h in enumerate(headers3, start=1):
cell = ws3.cell(row=hr3, column=c, value=h)
cell.font = HEADER_FONT
cell.fill = SECTION_FILL
cell.alignment = CENTER
cell.border = BORDER
ws3.row_dimensions[hr3].height = 40
widths3 = [11, 12, 14, 16, 14, 16, 16, 14, 20, 16, 30]
for i, w in enumerate(widths3, start=1):
ws3.column_dimensions[get_column_letter(i)].width = w
start_g, end_g = hr3 + 1, hr3 + 10 # Day 1-10
for i, r in enumerate(range(start_g, end_g + 1), start=1):
ws3.cell(row=r, column=1, value=f"Day {i}").font = SUBHEADER_FONT
ws3.cell(row=r, column=1).alignment = CENTER
for c in range(1, len(headers3) + 1):
cell = ws3.cell(row=r, column=c)
cell.border = BORDER
cell.font = BODY_FONT
cell.alignment = CENTER
if i % 2 == 0:
for c in range(2, len(headers3) + 1):
ws3.cell(row=r, column=c).fill = ALT_FILL
dv_yn = DataValidation(type="list", formula1='"Y,N"', allow_blank=True)
ws3.add_data_validation(dv_yn)
for col in ["C", "E", "G", "J"]:
dv_yn.add(f"{col}{start_g}:{col}{end_g}")
ws3.conditional_formatting.add(f"J{start_g}:J{end_g}", CellIsRule(operator="equal", formula=['"Y"'], fill=PASS_FILL))
ws3.conditional_formatting.add(f"J{start_g}:J{end_g}", CellIsRule(operator="equal", formula=['"N"'], fill=FAIL_FILL))
note_g = end_g + 2
ws3.merge_cells(f"A{note_g}:K{note_g}")
ws3[f"A{note_g}"] = ("Reference schedule (Faverio et al. protocol): Day1 = interrupt AM session; Day2 = interrupt AM+PM; "
"Day3 = interrupt all daytime sessions (nocturnal BiPAP only); Day4+ = discontinue if stable overnight. "
"Each on-BiPAP session should be >=3 hours or maximum tolerated; off-periods use O2 via nasal cannula/Venturi titrated to SpO2 88-92%.")
ws3[f"A{note_g}"].font = NOTE_FONT
ws3[f"A{note_g}"].alignment = LEFT
ws3.merge_cells(f"A{note_g+1}:K{note_g+1}")
ws3[f"A{note_g+1}"] = "Source: Faverio P, et al., summarized in Karim & Ashkenazi PMC12295356; Duan J, et al. protocol-directed NIV weaning."
ws3[f"A{note_g+1}"].font = NOTE_FONT
# =========================================================
# SHEET 4: Readiness & Failure Criteria (Reference) - dual units
# =========================================================
ws4 = wb.create_sheet("Readiness & Failure Criteria")
ws4.sheet_view.showGridLines = False
style_title(ws4, "A1:C1", "REFERENCE: BiPAP/NIV WEANING CLINICAL CRITERIA (mmHg and kPa)")
ws4.row_dimensions[1].height = 24
def add_section(ws, start_row, title, rows):
r = start_row
ws.merge_cells(f"A{r}:C{r}")
ws[f"A{r}"] = title
ws[f"A{r}"].font = HEADER_FONT
ws[f"A{r}"].fill = SECTION_FILL
ws[f"A{r}"].alignment = CENTER
r += 1
for row_vals in rows:
for c, val in enumerate(row_vals, start=1):
cell = ws.cell(row=r, column=c, value=val)
cell.font = BODY_FONT
cell.border = BORDER
cell.alignment = LEFT
r += 1
return r + 1
row_ptr = 3
row_ptr = add_section(ws4, row_ptr, "1. DAILY READINESS SCREEN (Baseline on BiPAP) - Must meet ALL to proceed", [
("Parameter", "Threshold (mmHg)", "Threshold (kPa)"),
("pH", "≥ 7.35", "≥ 7.35 (unitless)"),
("PaCO2", "Decreased ≥10% from pre-BiPAP baseline", "Decreased ≥10% from pre-BiPAP baseline"),
("PaO2 / SaO2", "PaO2 >60 mmHg or SaO2 ≥90% on FiO2 <50%", "PaO2 >8.0 kPa or SaO2 ≥90% on FiO2 <50%"),
("Respiratory rate", "<25-30 /min, no accessory muscle use", "-"),
("Heart rate", "50-120 bpm", "-"),
("Systolic BP", "90-180 mmHg without vasopressors", "12.0-24.0 kPa without vasopressors"),
("Mental status", "Alert, Kelly score ≤2, no sedation", "-"),
("Stability on BiPAP", "Clinically stable ≥6-24 h", "-"),
])
row_ptr = add_section(ws4, row_ptr, "2. SPONTANEOUS TRIAL OFF BiPAP (1-4 hrs on O2 via NC/Venturi) - Pass vs Fail", [
("Parameter", "Pass (continue weaning)", "Fail (resume BiPAP)"),
("pH", "≥ 7.35", "< 7.35"),
("PaCO2 rise", "< 20% above baseline", "≥ 20% above baseline"),
("SaO2", "88-92% on FiO2 ≤40%", "< 88-90%"),
("Respiratory rate", "8-30 /min", "< 8 or > 30 /min"),
("Heart rate", "50-120 bpm", "< 50 or > 120 bpm"),
("Dyspnea (Borg scale)", "≤ 4", "> 4 (severe)"),
("Consciousness", "Kelly score ≤2, stable", "Kelly score worsens ≥1 point"),
])
row_ptr = add_section(ws4, row_ptr, "3. MAJOR CRITERIA - REINSTITUTE BiPAP IMMEDIATELY (any one)", [
("Respiratory rate", "<8 or >30 /min", ""),
("Systolic BP", "<90 mmHg (12.0 kPa) or >180 mmHg (24.0 kPa) without vasopressors", ""),
("Heart rate", "<50 or >120 bpm", ""),
("Kelly score", ">2", ""),
("SaO2", "<90% on FiO2 ≥40%", ""),
("pH", "<7.35", ""),
("PaCO2", ">20% increase from weaning start (mmHg or kPa, same % rule)", ""),
("Dyspnea", "Severe (Borg >4)", ""),
])
row_ptr = add_section(ws4, row_ptr, "4. MINOR / GRADUAL FAILURE INDICATORS - Reassess closely", [
("PaO2/FiO2 ratio", "Reduction ≥30% from baseline", ""),
("PaCO2", "Increase ≥20%", ""),
("Alertness", "Worsening on Kelly scale (≥1 point)", ""),
("Respiratory distress", "New onset or persistent", ""),
("Fatigue", "Exhaustion", ""),
("Secretions", "Accumulation despite physio/cough assist", ""),
])
row_ptr = add_section(ws4, row_ptr, "5. EMERGENCY / INTUBATE - DO NOT ATTEMPT FURTHER WEANING", [
("Respiratory arrest", "", ""),
("Respiratory pauses with unconsciousness", "", ""),
("Severe hemodynamic instability", "", ""),
("Intolerance forcing device discontinuation", "", ""),
])
row_ptr = add_section(ws4, row_ptr, "6. UNIT CONVERSION QUICK REFERENCE", [
("mmHg to kPa", "multiply by 0.1333", ""),
("kPa to mmHg", "multiply by 7.5006", ""),
("Normal PaCO2", "35-45 mmHg = 4.7-6.0 kPa", ""),
("Normal PaO2", "80-100 mmHg = 10.6-13.3 kPa", ""),
])
ws4.column_dimensions["A"].width = 30
ws4.column_dimensions["B"].width = 42
ws4.column_dimensions["C"].width = 42
src_row = row_ptr
ws4.merge_cells(f"A{src_row}:C{src_row}")
ws4[f"A{src_row}"] = ("Sources: BTS/ICS 2016 NIV Guideline (via Pulmonology journal review); Duan J et al. protocol-directed NIV weaning; "
"Faverio P et al. NIV weaning algorithm; Washington Manual of Medical Therapeutics.")
ws4[f"A{src_row}"].font = NOTE_FONT
ws4[f"A{src_row}"].alignment = LEFT
# =========================================================
# SHEET 5: Instructions & Legend
# =========================================================
ws5 = wb.create_sheet("Instructions")
ws5.sheet_view.showGridLines = False
style_title(ws5, "A1:B1", "HOW TO USE THIS TRACKING WORKBOOK")
ws5.row_dimensions[1].height = 24
ws5.column_dimensions["A"].width = 26
ws5.column_dimensions["B"].width = 90
instructions = [
("Detailed Shift Log", "Use for full granular documentation - one row per shift/assessment (40 rows ~ covers an extended weaning course). "
"Enter ABG in mmHg; kPa columns auto-calculate (grey italic). Cells auto-highlight red if a value crosses an unsafe threshold."),
("7-Day EMR Summary", "Condensed alternative - one row per calendar day, grouped AM/PM/Night blocks, styled like an EMR flowsheet for a quick "
"7-day-at-a-glance view. Best when charting summary-level rather than every vital. kPa auto-calculates from mmHg here too."),
("Gradual Interruption Schedule", "Use once the patient passes the daily readiness screen. Track each on/off BiPAP session per the "
"gradual interruption protocol (AM -> AM+PM -> all daytime -> nocturnal only -> full discontinuation)."),
("Readiness & Failure Criteria", "Static clinical reference in both mmHg and kPa - keep visible while completing either log. Do not edit; "
"use as the source of truth for Pass/Fail and Major/Minor criteria decisions."),
("Color Key", "Green fill = criterion met / tolerated. Red fill = criterion failed / unsafe value - notify physician and resume BiPAP. "
"Grey italic = auto-calculated (formula) cell, do not overwrite."),
("Data Validation", "Dropdown lists are built into Shift, Readiness Screen, Trial/Overall Outcome, Kelly Score, and Y/N columns to standardize entries."),
("Choosing a duration", "Detailed Shift Log = longer/extended courses tracked shift-by-shift. 7-Day EMR Summary = short, standard 7-day weaning "
"course tracked once daily. Use either independently or both together."),
("Disclaimer", "This tool is a documentation aid based on published NIV weaning protocols. It does not replace institutional policy, "
"physician orders, or clinical judgment. Always follow your unit's approved NIV/BiPAP weaning protocol."),
]
r = 3
for label, text in instructions:
ws5.cell(row=r, column=1, value=label).font = SUBHEADER_FONT
ws5.cell(row=r, column=1).alignment = Alignment(vertical="top", wrap_text=True)
ws5.cell(row=r, column=1).border = BORDER
cell = ws5.cell(row=r, column=2, value=text)
cell.font = BODY_FONT
cell.alignment = Alignment(vertical="top", wrap_text=True)
cell.border = BORDER
ws5.row_dimensions[r].height = 55
r += 1
out_path = "/tmp/workspace/bipap-weaning/BiPAP_Weaning_Tracking_Log.xlsx"
wb.save(out_path)
print("Saved:", out_path)
rm /tmp/workspace/bipap-weaning/build_workbook.py
~/bipap-weaning/build_workbook.py
"""
Build a Daily BiPAP Weaning Tracking Log Excel workbook for Respiratory Therapists.
Sheets:
1. Detailed Shift Log - one row per shift/assessment (extended duration, ~40 rows), dual units (mmHg + kPa)
2. 7-Day EMR Summary - one row per calendar day, EMR flowsheet style, condensed AM/PM/Night blocks
3. Gradual Interruption Schedule - day-by-day on/off session tracker
4. Readiness & Failure Criteria (Reference) - static clinical reference table, dual units
5. Instructions & Legend
"""
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.worksheet.datavalidation import DataValidation
from openpyxl.utils import get_column_letter
from openpyxl.formatting.rule import CellIsRule, FormulaRule
FONT_NAME = "Arial"
MMHG_TO_KPA = 0.133322
# ---- Style constants ----
TITLE_FONT = Font(name=FONT_NAME, size=14, bold=True, color="FFFFFF")
HEADER_FONT = Font(name=FONT_NAME, size=10, bold=True, color="FFFFFF")
SUBHEADER_FONT = Font(name=FONT_NAME, size=10, bold=True, color="000000")
BODY_FONT = Font(name=FONT_NAME, size=10)
CALC_FONT = Font(name=FONT_NAME, size=10, color="000000", italic=True) # computed / converted cells
NOTE_FONT = Font(name=FONT_NAME, size=9, italic=True, color="595959")
HEADER_FILL = PatternFill("solid", fgColor="1F4E78")
SECTION_FILL = PatternFill("solid", fgColor="2E75B6")
GROUP_FILL = PatternFill("solid", fgColor="8EA9DB")
ALT_FILL = PatternFill("solid", fgColor="D9E1F2")
PASS_FILL = PatternFill("solid", fgColor="C6EFCE")
FAIL_FILL = PatternFill("solid", fgColor="FFC7CE")
CALC_FILL = PatternFill("solid", fgColor="F2F2F2")
THIN = Side(style="thin", color="B7B7B7")
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True)
LEFT = Alignment(horizontal="left", vertical="center", wrap_text=True)
def style_title(ws, cell_range, text):
ws.merge_cells(cell_range)
top_left = cell_range.split(":")[0]
ws[top_left] = text
ws[top_left].font = TITLE_FONT
ws[top_left].fill = HEADER_FILL
ws[top_left].alignment = CENTER
wb = Workbook()
# =========================================================
# SHEET 1: Detailed Shift Log (extended duration, dual units)
# =========================================================
ws = wb.active
ws.title = "Detailed Shift Log"
ws.sheet_view.showGridLines = False
style_title(ws, "A1:X1", "BiPAP / NIV DETAILED SHIFT-BY-SHIFT WEANING LOG")
ws.row_dimensions[1].height = 26
ws.merge_cells("A2:X2")
ws["A2"] = "Patient Name: _____________________ MRN: _____________ Unit/Bed: _______ Admit Dx: ________________________"
ws["A2"].font = Font(name=FONT_NAME, size=10, bold=True)
ws["A2"].alignment = LEFT
ws.row_dimensions[2].height = 20
headers = [
"Date", "Shift\n(Day/Eve/Night)", "Time", "RT Initials",
"IPAP\n(cmH2O)", "EPAP\n(cmH2O)", "FiO2\n(%)",
"RR\n(/min)", "HR\n(bpm)", "SpO2\n(%)", "Systolic BP\n(mmHg)",
"ABG pH",
"PaCO2\n(mmHg)", "PaCO2\n(kPa, auto)",
"PaO2\n(mmHg)", "PaO2\n(kPa, auto)",
"Kelly Score\n(0-2 pass)", "Borg Dyspnea\n(0-10)",
"Readiness\nScreen (Pass/Fail)", "Trial Off Duration\n(hrs)",
"Trial Outcome\n(Tolerated/Failed)", "Failure Criteria Met\n(list or None)",
"Plan / Next Step", "RT Signature"
]
header_row = 3
for c, h in enumerate(headers, start=1):
cell = ws.cell(row=header_row, column=c, value=h)
cell.font = HEADER_FONT
cell.fill = HEADER_FILL
cell.alignment = CENTER
cell.border = BORDER
ws.row_dimensions[header_row].height = 42
col_widths = [11, 12, 8, 10, 9, 9, 8, 8, 8, 8, 11, 9, 10, 11, 10, 11, 11, 11, 13, 13, 14, 20, 26, 14]
for i, w in enumerate(col_widths, start=1):
ws.column_dimensions[get_column_letter(i)].width = w
DATA_START = header_row + 1
DATA_END = DATA_START + 39 # 40 blank rows for logging (roughly a month of shifts)
KPA_CO2_COL = 14 # column N
KPA_O2_COL = 16 # column P
MMHG_CO2_COL = 13
MMHG_O2_COL = 15
for r in range(DATA_START, DATA_END + 1):
for c in range(1, len(headers) + 1):
cell = ws.cell(row=r, column=c)
cell.border = BORDER
cell.font = BODY_FONT
cell.alignment = CENTER
if (r - DATA_START) % 2 == 1:
for c in range(1, len(headers) + 1):
ws.cell(row=r, column=c).fill = ALT_FILL
# Auto-convert mmHg -> kPa (computed, non-editable style)
co2_mm = f"{get_column_letter(MMHG_CO2_COL)}{r}"
o2_mm = f"{get_column_letter(MMHG_O2_COL)}{r}"
ws.cell(row=r, column=KPA_CO2_COL, value=f'=IF({co2_mm}="","",ROUND({co2_mm}*{MMHG_TO_KPA},2))')
ws.cell(row=r, column=KPA_O2_COL, value=f'=IF({o2_mm}="","",ROUND({o2_mm}*{MMHG_TO_KPA},2))')
ws.cell(row=r, column=KPA_CO2_COL).font = CALC_FONT
ws.cell(row=r, column=KPA_O2_COL).font = CALC_FONT
ws.cell(row=r, column=KPA_CO2_COL).fill = CALC_FILL
ws.cell(row=r, column=KPA_O2_COL).fill = CALC_FILL
# Data validations
dv_shift = DataValidation(type="list", formula1='"Day,Evening,Night"', allow_blank=True)
dv_screen = DataValidation(type="list", formula1='"Pass,Fail"', allow_blank=True)
dv_outcome = DataValidation(type="list", formula1='"Tolerated,Failed,N/A - Continuous BiPAP"', allow_blank=True)
dv_kelly = DataValidation(type="list", formula1='"0,1,2,3,4,5"', allow_blank=True)
for dv in (dv_shift, dv_screen, dv_outcome, dv_kelly):
ws.add_data_validation(dv)
dv_shift.add(f"B{DATA_START}:B{DATA_END}")
dv_screen.add(f"S{DATA_START}:S{DATA_END}")
dv_outcome.add(f"U{DATA_START}:U{DATA_END}")
dv_kelly.add(f"Q{DATA_START}:Q{DATA_END}")
# Conditional formatting
ws.conditional_formatting.add(f"S{DATA_START}:S{DATA_END}", CellIsRule(operator="equal", formula=['"Pass"'], fill=PASS_FILL))
ws.conditional_formatting.add(f"S{DATA_START}:S{DATA_END}", CellIsRule(operator="equal", formula=['"Fail"'], fill=FAIL_FILL))
ws.conditional_formatting.add(f"U{DATA_START}:U{DATA_END}", CellIsRule(operator="equal", formula=['"Tolerated"'], fill=PASS_FILL))
ws.conditional_formatting.add(f"U{DATA_START}:U{DATA_END}", CellIsRule(operator="equal", formula=['"Failed"'], fill=FAIL_FILL))
ws.conditional_formatting.add(f"H{DATA_START}:H{DATA_END}", FormulaRule(formula=[f"OR(H{DATA_START}<8,H{DATA_START}>30)"], fill=FAIL_FILL))
ws.conditional_formatting.add(f"I{DATA_START}:I{DATA_END}", FormulaRule(formula=[f"OR(I{DATA_START}<50,I{DATA_START}>120)"], fill=FAIL_FILL))
ws.conditional_formatting.add(f"J{DATA_START}:J{DATA_END}", FormulaRule(formula=[f"J{DATA_START}<88"], fill=FAIL_FILL))
ws.conditional_formatting.add(f"L{DATA_START}:L{DATA_END}", FormulaRule(formula=[f"L{DATA_START}<7.35"], fill=FAIL_FILL))
ws.freeze_panes = f"A{DATA_START}"
note_row = DATA_END + 2
ws.merge_cells(f"A{note_row}:X{note_row}")
ws[f"A{note_row}"] = ("Legend: Enter PaCO2/PaO2 in mmHg - kPa columns auto-calculate (grey italic, formula-driven, do not overwrite). "
"Readiness Screen = daily bedside criteria per BTS/ICS guideline (see 'Readiness & Failure Criteria' tab). "
"Red fill = value outside safe threshold - reassess and notify physician.")
ws[f"A{note_row}"].font = NOTE_FONT
ws[f"A{note_row}"].alignment = LEFT
# =========================================================
# SHEET 2: 7-Day EMR Summary (single row per day, condensed flowsheet style)
# =========================================================
ws2 = wb.create_sheet("7-Day EMR Summary")
ws2.sheet_view.showGridLines = False
style_title(ws2, "A1:S1", "BiPAP WEANING - 7-DAY EMR-STYLE SUMMARY FLOWSHEET (One Row per Day)")
ws2.row_dimensions[1].height = 24
ws2.merge_cells("A2:S2")
ws2["A2"] = "Patient Name: _____________________ MRN: _____________ Weaning Start Date: ___________"
ws2["A2"].font = Font(name=FONT_NAME, size=10, bold=True)
ws2.row_dimensions[2].height = 20
group_row, sub_row = 3, 4
# Build columns explicitly for clarity
col_defs = [
# (group label or None, sub label, width)
(None, "Date", 11),
(None, "Weaning\nDay #", 8),
("AM SHIFT", "Settings\n(IPAP/EPAP/FiO2)", 16),
("AM SHIFT", "Vitals\n(RR/HR/SpO2)", 14),
("AM SHIFT", "Hrs Off\nBiPAP", 8),
("PM SHIFT", "Settings\n(IPAP/EPAP/FiO2)", 16),
("PM SHIFT", "Vitals\n(RR/HR/SpO2)", 14),
("PM SHIFT", "Hrs Off\nBiPAP", 8),
("NIGHT SHIFT", "Settings\n(IPAP/EPAP/FiO2)", 16),
("NIGHT SHIFT", "Vitals\n(RR/HR/SpO2)", 14),
("NIGHT SHIFT", "Hrs Off\nBiPAP", 8),
("DAILY ABG", "pH", 8),
("DAILY ABG", "PaCO2\n(mmHg)", 9),
("DAILY ABG", "PaCO2\n(kPa, auto)", 10),
("DAILY ABG", "PaO2\n(mmHg)", 9),
("DAILY ABG", "PaO2\n(kPa, auto)", 10),
(None, "Readiness\nScreen (P/F)", 12),
(None, "Total Hrs\nOff /24h", 9),
(None, "Overall\nOutcome", 13),
(None, "Plan / RT Sign-off", 26),
]
n_cols = len(col_defs)
for i, (grp, sub, width) in enumerate(col_defs, start=1):
ws2.column_dimensions[get_column_letter(i)].width = width
# Write group header row with merges for contiguous same-group cells
i = 1
while i <= n_cols:
grp = col_defs[i - 1][0]
if grp is None:
cell = ws2.cell(row=group_row, column=i, value="")
cell.fill = HEADER_FILL
cell.border = BORDER
i += 1
continue
j = i
while j <= n_cols and col_defs[j - 1][0] == grp:
j += 1
if j - 1 > i:
ws2.merge_cells(start_row=group_row, start_column=i, end_row=group_row, end_column=j - 1)
cell = ws2.cell(row=group_row, column=i, value=grp)
cell.font = HEADER_FONT
cell.fill = SECTION_FILL if "SHIFT" not in grp else GROUP_FILL
cell.alignment = CENTER
cell.border = BORDER
for cc in range(i, j):
ws2.cell(row=group_row, column=cc).border = BORDER
ws2.cell(row=group_row, column=cc).fill = SECTION_FILL if "SHIFT" not in grp else GROUP_FILL
i = j
ws2.row_dimensions[group_row].height = 18
for c, (grp, sub, width) in enumerate(col_defs, start=1):
cell = ws2.cell(row=sub_row, column=c, value=sub)
cell.font = HEADER_FONT
cell.fill = HEADER_FILL
cell.alignment = CENTER
cell.border = BORDER
ws2.row_dimensions[sub_row].height = 36
start3 = sub_row + 1
end3 = start3 + 6 # 7 days
DATE_COL, DAY_COL = 1, 2
ABG_PH_COL = 12
ABG_CO2_MM_COL = 13
ABG_CO2_KPA_COL = 14
ABG_O2_MM_COL = 15
ABG_O2_KPA_COL = 16
SCREEN_COL = 17
TOTALHRS_COL = 18
OUTCOME_COL = 19
PLAN_COL = 20
for idx, r in enumerate(range(start3, end3 + 1), start=1):
ws2.cell(row=r, column=DAY_COL, value=f"Day {idx}").font = SUBHEADER_FONT
for c in range(1, n_cols + 1):
cell = ws2.cell(row=r, column=c)
cell.border = BORDER
cell.font = BODY_FONT
cell.alignment = CENTER
if idx % 2 == 0:
for c in range(1, n_cols + 1):
ws2.cell(row=r, column=c).fill = ALT_FILL
co2_mm = f"{get_column_letter(ABG_CO2_MM_COL)}{r}"
o2_mm = f"{get_column_letter(ABG_O2_MM_COL)}{r}"
ws2.cell(row=r, column=ABG_CO2_KPA_COL, value=f'=IF({co2_mm}="","",ROUND({co2_mm}*{MMHG_TO_KPA},2))')
ws2.cell(row=r, column=ABG_O2_KPA_COL, value=f'=IF({o2_mm}="","",ROUND({o2_mm}*{MMHG_TO_KPA},2))')
ws2.cell(row=r, column=ABG_CO2_KPA_COL).font = CALC_FONT
ws2.cell(row=r, column=ABG_O2_KPA_COL).font = CALC_FONT
ws2.cell(row=r, column=ABG_CO2_KPA_COL).fill = CALC_FILL
ws2.cell(row=r, column=ABG_O2_KPA_COL).fill = CALC_FILL
dv_screen2 = DataValidation(type="list", formula1='"Pass,Fail"', allow_blank=True)
dv_outcome2 = DataValidation(type="list", formula1='"Fully Weaned,Partial - Nocturnal Only,Not Tolerated - Resumed BiPAP"', allow_blank=True)
ws2.add_data_validation(dv_screen2)
ws2.add_data_validation(dv_outcome2)
dv_screen2.add(f"{get_column_letter(SCREEN_COL)}{start3}:{get_column_letter(SCREEN_COL)}{end3}")
dv_outcome2.add(f"{get_column_letter(OUTCOME_COL)}{start3}:{get_column_letter(OUTCOME_COL)}{end3}")
sc = get_column_letter(SCREEN_COL)
oc = get_column_letter(OUTCOME_COL)
ws2.conditional_formatting.add(f"{sc}{start3}:{sc}{end3}", CellIsRule(operator="equal", formula=['"Pass"'], fill=PASS_FILL))
ws2.conditional_formatting.add(f"{sc}{start3}:{sc}{end3}", CellIsRule(operator="equal", formula=['"Fail"'], fill=FAIL_FILL))
ws2.conditional_formatting.add(f"{oc}{start3}:{oc}{end3}", CellIsRule(operator="equal", formula=['"Fully Weaned"'], fill=PASS_FILL))
ws2.conditional_formatting.add(f"{oc}{start3}:{oc}{end3}", CellIsRule(operator="equal", formula=['"Not Tolerated - Resumed BiPAP"'], fill=FAIL_FILL))
ph_c = get_column_letter(ABG_PH_COL)
ws2.conditional_formatting.add(f"{ph_c}{start3}:{ph_c}{end3}", FormulaRule(formula=[f"{ph_c}{start3}<7.35"], fill=FAIL_FILL))
ws2.freeze_panes = f"A{start3}"
note3 = end3 + 2
ws2.merge_cells(f"A{note3}:{get_column_letter(n_cols)}{note3}")
ws2[f"A{note3}"] = ("EMR-style condensed flowsheet: one row = one calendar day. Combine each shift's settings/vitals into a single cell "
"(e.g., '10/8, RR18 HR88 SpO2 94'). Designed for quick 7-day-at-a-glance review or copy/paste into EMR notes. "
"For full granular per-shift documentation use the 'Detailed Shift Log' tab instead.")
ws2[f"A{note3}"].font = NOTE_FONT
ws2[f"A{note3}"].alignment = LEFT
# =========================================================
# SHEET 3: Gradual Interruption Schedule
# =========================================================
ws3 = wb.create_sheet("Gradual Interruption Schedule")
ws3.sheet_view.showGridLines = False
style_title(ws3, "A1:K1", "BiPAP GRADUAL INTERRUPTION / SESSION TRACKER")
ws3.row_dimensions[1].height = 24
ws3.merge_cells("A2:K2")
ws3["A2"] = "Patient Name: _____________________ MRN: _____________ Weaning Start Date: ___________"
ws3["A2"].font = Font(name=FONT_NAME, size=10, bold=True)
ws3.row_dimensions[2].height = 20
headers3 = [
"Weaning Day", "Date", "Morning Session\nOff? (Y/N)", "Morning Off Duration (hrs)",
"Afternoon Session\nOff? (Y/N)", "Afternoon Off Duration (hrs)",
"Evening/Night Session\nOff? (Y/N)", "Night Off Duration (hrs)",
"SpO2 Range During Off-Periods (%)", "Tolerated Overall (Y/N)", "RT Notes"
]
hr3 = 3
for c, h in enumerate(headers3, start=1):
cell = ws3.cell(row=hr3, column=c, value=h)
cell.font = HEADER_FONT
cell.fill = SECTION_FILL
cell.alignment = CENTER
cell.border = BORDER
ws3.row_dimensions[hr3].height = 40
widths3 = [11, 12, 14, 16, 14, 16, 16, 14, 20, 16, 30]
for i, w in enumerate(widths3, start=1):
ws3.column_dimensions[get_column_letter(i)].width = w
start_g, end_g = hr3 + 1, hr3 + 10 # Day 1-10
for i, r in enumerate(range(start_g, end_g + 1), start=1):
ws3.cell(row=r, column=1, value=f"Day {i}").font = SUBHEADER_FONT
ws3.cell(row=r, column=1).alignment = CENTER
for c in range(1, len(headers3) + 1):
cell = ws3.cell(row=r, column=c)
cell.border = BORDER
cell.font = BODY_FONT
cell.alignment = CENTER
if i % 2 == 0:
for c in range(2, len(headers3) + 1):
ws3.cell(row=r, column=c).fill = ALT_FILL
dv_yn = DataValidation(type="list", formula1='"Y,N"', allow_blank=True)
ws3.add_data_validation(dv_yn)
for col in ["C", "E", "G", "J"]:
dv_yn.add(f"{col}{start_g}:{col}{end_g}")
ws3.conditional_formatting.add(f"J{start_g}:J{end_g}", CellIsRule(operator="equal", formula=['"Y"'], fill=PASS_FILL))
ws3.conditional_formatting.add(f"J{start_g}:J{end_g}", CellIsRule(operator="equal", formula=['"N"'], fill=FAIL_FILL))
note_g = end_g + 2
ws3.merge_cells(f"A{note_g}:K{note_g}")
ws3[f"A{note_g}"] = ("Reference schedule (Faverio et al. protocol): Day1 = interrupt AM session; Day2 = interrupt AM+PM; "
"Day3 = interrupt all daytime sessions (nocturnal BiPAP only); Day4+ = discontinue if stable overnight. "
"Each on-BiPAP session should be >=3 hours or maximum tolerated; off-periods use O2 via nasal cannula/Venturi titrated to SpO2 88-92%.")
ws3[f"A{note_g}"].font = NOTE_FONT
ws3[f"A{note_g}"].alignment = LEFT
ws3.merge_cells(f"A{note_g+1}:K{note_g+1}")
ws3[f"A{note_g+1}"] = "Source: Faverio P, et al., summarized in Karim & Ashkenazi PMC12295356; Duan J, et al. protocol-directed NIV weaning."
ws3[f"A{note_g+1}"].font = NOTE_FONT
# =========================================================
# SHEET 4: Readiness & Failure Criteria (Reference) - dual units
# =========================================================
ws4 = wb.create_sheet("Readiness & Failure Criteria")
ws4.sheet_view.showGridLines = False
style_title(ws4, "A1:C1", "REFERENCE: BiPAP/NIV WEANING CLINICAL CRITERIA (mmHg and kPa)")
ws4.row_dimensions[1].height = 24
def add_section(ws, start_row, title, rows):
r = start_row
ws.merge_cells(f"A{r}:C{r}")
ws[f"A{r}"] = title
ws[f"A{r}"].font = HEADER_FONT
ws[f"A{r}"].fill = SECTION_FILL
ws[f"A{r}"].alignment = CENTER
r += 1
for row_vals in rows:
for c, val in enumerate(row_vals, start=1):
cell = ws.cell(row=r, column=c, value=val)
cell.font = BODY_FONT
cell.border = BORDER
cell.alignment = LEFT
r += 1
return r + 1
row_ptr = 3
row_ptr = add_section(ws4, row_ptr, "1. DAILY READINESS SCREEN (Baseline on BiPAP) - Must meet ALL to proceed", [
("Parameter", "Threshold (mmHg)", "Threshold (kPa)"),
("pH", "≥ 7.35", "≥ 7.35 (unitless)"),
("PaCO2", "Decreased ≥10% from pre-BiPAP baseline", "Decreased ≥10% from pre-BiPAP baseline"),
("PaO2 / SaO2", "PaO2 >60 mmHg or SaO2 ≥90% on FiO2 <50%", "PaO2 >8.0 kPa or SaO2 ≥90% on FiO2 <50%"),
("Respiratory rate", "<25-30 /min, no accessory muscle use", "-"),
("Heart rate", "50-120 bpm", "-"),
("Systolic BP", "90-180 mmHg without vasopressors", "12.0-24.0 kPa without vasopressors"),
("Mental status", "Alert, Kelly score ≤2, no sedation", "-"),
("Stability on BiPAP", "Clinically stable ≥6-24 h", "-"),
])
row_ptr = add_section(ws4, row_ptr, "2. SPONTANEOUS TRIAL OFF BiPAP (1-4 hrs on O2 via NC/Venturi) - Pass vs Fail", [
("Parameter", "Pass (continue weaning)", "Fail (resume BiPAP)"),
("pH", "≥ 7.35", "< 7.35"),
("PaCO2 rise", "< 20% above baseline", "≥ 20% above baseline"),
("SaO2", "88-92% on FiO2 ≤40%", "< 88-90%"),
("Respiratory rate", "8-30 /min", "< 8 or > 30 /min"),
("Heart rate", "50-120 bpm", "< 50 or > 120 bpm"),
("Dyspnea (Borg scale)", "≤ 4", "> 4 (severe)"),
("Consciousness", "Kelly score ≤2, stable", "Kelly score worsens ≥1 point"),
])
row_ptr = add_section(ws4, row_ptr, "3. MAJOR CRITERIA - REINSTITUTE BiPAP IMMEDIATELY (any one)", [
("Respiratory rate", "<8 or >30 /min", ""),
("Systolic BP", "<90 mmHg (12.0 kPa) or >180 mmHg (24.0 kPa) without vasopressors", ""),
("Heart rate", "<50 or >120 bpm", ""),
("Kelly score", ">2", ""),
("SaO2", "<90% on FiO2 ≥40%", ""),
("pH", "<7.35", ""),
("PaCO2", ">20% increase from weaning start (mmHg or kPa, same % rule)", ""),
("Dyspnea", "Severe (Borg >4)", ""),
])
row_ptr = add_section(ws4, row_ptr, "4. MINOR / GRADUAL FAILURE INDICATORS - Reassess closely", [
("PaO2/FiO2 ratio", "Reduction ≥30% from baseline", ""),
("PaCO2", "Increase ≥20%", ""),
("Alertness", "Worsening on Kelly scale (≥1 point)", ""),
("Respiratory distress", "New onset or persistent", ""),
("Fatigue", "Exhaustion", ""),
("Secretions", "Accumulation despite physio/cough assist", ""),
])
row_ptr = add_section(ws4, row_ptr, "5. EMERGENCY / INTUBATE - DO NOT ATTEMPT FURTHER WEANING", [
("Respiratory arrest", "", ""),
("Respiratory pauses with unconsciousness", "", ""),
("Severe hemodynamic instability", "", ""),
("Intolerance forcing device discontinuation", "", ""),
])
row_ptr = add_section(ws4, row_ptr, "6. UNIT CONVERSION QUICK REFERENCE", [
("mmHg to kPa", "multiply by 0.1333", ""),
("kPa to mmHg", "multiply by 7.5006", ""),
("Normal PaCO2", "35-45 mmHg = 4.7-6.0 kPa", ""),
("Normal PaO2", "80-100 mmHg = 10.6-13.3 kPa", ""),
])
ws4.column_dimensions["A"].width = 30
ws4.column_dimensions["B"].width = 42
ws4.column_dimensions["C"].width = 42
src_row = row_ptr
ws4.merge_cells(f"A{src_row}:C{src_row}")
ws4[f"A{src_row}"] = ("Sources: BTS/ICS 2016 NIV Guideline (via Pulmonology journal review); Duan J et al. protocol-directed NIV weaning; "
"Faverio P et al. NIV weaning algorithm; Washington Manual of Medical Therapeutics.")
ws4[f"A{src_row}"].font = NOTE_FONT
ws4[f"A{src_row}"].alignment = LEFT
# =========================================================
# SHEET 5: Instructions & Legend
# =========================================================
ws5 = wb.create_sheet("Instructions")
ws5.sheet_view.showGridLines = False
style_title(ws5, "A1:B1", "HOW TO USE THIS TRACKING WORKBOOK")
ws5.row_dimensions[1].height = 24
ws5.column_dimensions["A"].width = 26
ws5.column_dimensions["B"].width = 90
instructions = [
("Detailed Shift Log", "Use for full granular documentation - one row per shift/assessment (40 rows ~ covers an extended weaning course). "
"Enter ABG in mmHg; kPa columns auto-calculate (grey italic). Cells auto-highlight red if a value crosses an unsafe threshold."),
("7-Day EMR Summary", "Condensed alternative - one row per calendar day, grouped AM/PM/Night blocks, styled like an EMR flowsheet for a quick "
"7-day-at-a-glance view. Best when charting summary-level rather than every vital. kPa auto-calculates from mmHg here too."),
("Gradual Interruption Schedule", "Use once the patient passes the daily readiness screen. Track each on/off BiPAP session per the "
"gradual interruption protocol (AM -> AM+PM -> all daytime -> nocturnal only -> full discontinuation)."),
("Readiness & Failure Criteria", "Static clinical reference in both mmHg and kPa - keep visible while completing either log. Do not edit; "
"use as the source of truth for Pass/Fail and Major/Minor criteria decisions."),
("Color Key", "Green fill = criterion met / tolerated. Red fill = criterion failed / unsafe value - notify physician and resume BiPAP. "
"Grey italic = auto-calculated (formula) cell, do not overwrite."),
("Data Validation", "Dropdown lists are built into Shift, Readiness Screen, Trial/Overall Outcome, Kelly Score, and Y/N columns to standardize entries."),
("Choosing a duration", "Detailed Shift Log = longer/extended courses tracked shift-by-shift. 7-Day EMR Summary = short, standard 7-day weaning "
"course tracked once daily. Use either independently or both together."),
("Disclaimer", "This tool is a documentation aid based on published NIV weaning protocols. It does not replace institutional policy, "
"physician orders, or clinical judgment. Always follow your unit's approved NIV/BiPAP weaning protocol."),
]
r = 3
for label, text in instructions:
ws5.cell(row=r, column=1, value=label).font = SUBHEADER_FONT
ws5.cell(row=r, column=1).alignment = Alignment(vertical="top", wrap_text=True)
ws5.cell(row=r, column=1).border = BORDER
cell = ws5.cell(row=r, column=2, value=text)
cell.font = BODY_FONT
cell.alignment = Alignment(vertical="top", wrap_text=True)
cell.border = BORDER
ws5.row_dimensions[r].height = 55
r += 1
out_path = "/tmp/workspace/bipap-weaning/BiPAP_Weaning_Tracking_Log.xlsx"
wb.save(out_path)
print("Saved:", out_path)
cd /tmp/workspace/bipap-weaning && python3 build_workbook.py
cd /tmp/workspace/bipap-weaning && python3 -c " import openpyxl wb = openpyxl.load_workbook('BiPAP_Weaning_Tracking_Log.xlsx') print(wb.sheetnames) for s in wb.sheetnames: ws = wb[s] print(s, ws.dimensions) print('Sample formula:', wb['Detailed Shift Log']['N4'].value) print('Sample formula2:', wb['7-Day EMR Summary']['N5'].value) " ls -la /tmp/workspace/bipap-weaning/
BiPAP Weaning Tracking Log
Spreadsheet · XLSX
=IF(mmHg="","",ROUND(mmHg*0.133322,2))).Create a printable PDF quick-reference card for bedside BiPAP weaning criteria