feat: Enhance medical report generation with new features and improved data handling
- Added body fat percentage input and optional muscle oxygenation CSV upload in the upload form. - Implemented TSI chart generation based on muscle oxygenation data. - Updated report generation to include metabolism and fuel source charts. - Refactored context generation to eliminate reliance on SECA data, using patient info directly instead. - Improved error handling and logging for graph generation processes. - Enhanced HTML templates for better user experience and functionality.
This commit is contained in:
@@ -6,7 +6,7 @@ of the medical report. It performs analysis on Pnoe, Spirometry, and SECA data.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
import pandas as pd
|
||||
|
||||
@@ -24,12 +24,15 @@ class ContextGenerator:
|
||||
self,
|
||||
pnoe_path: str,
|
||||
spirometry_path: str,
|
||||
seca_path: str,
|
||||
seca_path: Optional[str] = None,
|
||||
):
|
||||
"""Load all required datasets"""
|
||||
self.pnoe_df = pd.read_csv(pnoe_path, delimiter=";")
|
||||
self.spirometry_df = pd.read_csv(spirometry_path)
|
||||
self.seca_df = pd.read_excel(seca_path)
|
||||
if seca_path:
|
||||
self.seca_df = pd.read_excel(seca_path)
|
||||
else:
|
||||
self.seca_df = None
|
||||
self._preprocess_pnoe_data()
|
||||
|
||||
def _preprocess_pnoe_data(self):
|
||||
@@ -75,7 +78,7 @@ class ContextGenerator:
|
||||
)
|
||||
|
||||
def extract_patient_info(self, patient_name: str) -> Dict:
|
||||
"""Extract patient information from SECA dataset"""
|
||||
"""Extract patient information from SECA dataset or use provided patient_info"""
|
||||
if self.seca_df is not None:
|
||||
patient_data = self.seca_df[
|
||||
self.seca_df["LastName"].str.contains(
|
||||
@@ -99,49 +102,73 @@ class ContextGenerator:
|
||||
"fat_mass_lbs": weight_kg * fat_pct / 100 * 2.20462,
|
||||
"lean_mass_lbs": weight_kg * (1 - fat_pct / 100) * 2.20462,
|
||||
}
|
||||
# If patient_info is already set (from manual input), calculate fat_mass and lean_mass
|
||||
elif "weight" in self.patient_info and "fat_percentage" in self.patient_info:
|
||||
weight_kg = self.patient_info["weight"]
|
||||
fat_pct = self.patient_info["fat_percentage"]
|
||||
self.patient_info["fat_mass_lbs"] = weight_kg * fat_pct / 100 * 2.20462
|
||||
self.patient_info["lean_mass_lbs"] = (
|
||||
weight_kg * (1 - fat_pct / 100) * 2.20462
|
||||
)
|
||||
return self.patient_info
|
||||
|
||||
def calculate_spirometry_metrics(self, metric_overrides: Optional[Dict] = None) -> Dict:
|
||||
def calculate_spirometry_metrics(
|
||||
self, metric_overrides: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""Calculate spirometry-related metrics"""
|
||||
if metric_overrides is None:
|
||||
metric_overrides = {}
|
||||
|
||||
|
||||
metrics = {}
|
||||
for param in ["FVC", "FEV1", "FEV1/FVC%"]:
|
||||
param_key = param.lower().replace("/", "_").replace("%", "_pct")
|
||||
|
||||
|
||||
if f"{param_key}_best" in metric_overrides:
|
||||
metrics[f"{param_key}_best"] = float(metric_overrides[f"{param_key}_best"])
|
||||
metrics[f"{param_key}_best"] = float(
|
||||
metric_overrides[f"{param_key}_best"]
|
||||
)
|
||||
else:
|
||||
row = self.spirometry_df.loc[
|
||||
self.spirometry_df["Parameters"].str.strip() == param
|
||||
]
|
||||
if not row.empty:
|
||||
metrics[f"{param_key}_best"] = row["Best"].values[0]
|
||||
|
||||
value = row["Best"].values[0]
|
||||
if pd.notna(value):
|
||||
try:
|
||||
metrics[f"{param_key}_best"] = float(value)
|
||||
except (ValueError, TypeError):
|
||||
pass # Skip if conversion fails
|
||||
|
||||
if f"{param_key}_pred" in metric_overrides:
|
||||
metrics[f"{param_key}_pred"] = float(metric_overrides[f"{param_key}_pred"])
|
||||
metrics[f"{param_key}_pred"] = float(
|
||||
metric_overrides[f"{param_key}_pred"]
|
||||
)
|
||||
else:
|
||||
row = self.spirometry_df.loc[
|
||||
self.spirometry_df["Parameters"].str.strip() == param
|
||||
]
|
||||
if not row.empty:
|
||||
metrics[f"{param_key}_pred"] = row["%Pred."].values[0]
|
||||
value = row["%Pred."].values[0]
|
||||
if pd.notna(value):
|
||||
try:
|
||||
metrics[f"{param_key}_pred"] = float(value)
|
||||
except (ValueError, TypeError):
|
||||
pass # Skip if conversion fails
|
||||
return metrics
|
||||
|
||||
def calculate_pnoe_metrics(self, metric_overrides: Optional[Dict] = None) -> Dict:
|
||||
"""Calculate all Pnoe-derived metrics"""
|
||||
if metric_overrides is None:
|
||||
metric_overrides = {}
|
||||
|
||||
|
||||
metrics = {}
|
||||
|
||||
|
||||
# VO2 Max metrics
|
||||
if "vo2_max" in metric_overrides:
|
||||
metrics["vo2_max"] = float(metric_overrides["vo2_max"])
|
||||
else:
|
||||
metrics["vo2_max"] = self.pnoe_df["VO2(ml/min)_smoothed"].max()
|
||||
|
||||
|
||||
if "vo2_max_per_kg" in metric_overrides:
|
||||
metrics["vo2_max_per_kg"] = float(metric_overrides["vo2_max_per_kg"])
|
||||
else:
|
||||
@@ -184,7 +211,7 @@ class ContextGenerator:
|
||||
else:
|
||||
vt1, _ = self._detect_thresholds()
|
||||
metrics["vt1"] = vt1
|
||||
|
||||
|
||||
if "vt2" in metric_overrides:
|
||||
metrics["vt2"] = metric_overrides["vt2"]
|
||||
else:
|
||||
@@ -200,9 +227,11 @@ class ContextGenerator:
|
||||
else:
|
||||
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
|
||||
fat_max_row = self.pnoe_df.loc[fat_max_idx]
|
||||
zones = self._calculate_hr_zones(metrics["vt1"], metrics["vt2"], fat_max_row)
|
||||
zones = self._calculate_hr_zones(
|
||||
metrics["vt1"], metrics["vt2"], fat_max_row
|
||||
)
|
||||
metrics.update(zones)
|
||||
|
||||
|
||||
return metrics
|
||||
|
||||
def _detect_thresholds(self) -> Tuple[Optional[Dict], Optional[Dict]]:
|
||||
@@ -261,95 +290,463 @@ class ContextGenerator:
|
||||
zones["zone5_bpm"] = f"{int(max_hr * 0.95)}+bpm"
|
||||
return zones
|
||||
|
||||
def _calculate_vo2_drop_points(self, pnoe_metrics: Dict) -> Dict:
|
||||
"""Calculate VO2 Pulse and VO2 Breath drop points"""
|
||||
# Calculate slope of VO2 Pulse
|
||||
vo2_pulse_slope = self.pnoe_df["VO2 Pulse_smoothed"].diff()
|
||||
window = max(1, len(self.pnoe_df) // 3) # Ensure window is at least 1
|
||||
vo2_pulse_slope_smoothed = vo2_pulse_slope.rolling(window=window, min_periods=1).mean()
|
||||
|
||||
# Find where VO2 Pulse begins to drop (slope becomes negative)
|
||||
mask_pulse = vo2_pulse_slope_smoothed <= 0
|
||||
drop_indices_pulse = mask_pulse[mask_pulse].index
|
||||
|
||||
vo2_pulse_drop_bpm = None
|
||||
vo2_pulse_drop_zone = None
|
||||
if len(drop_indices_pulse) > 0:
|
||||
drop_idx = drop_indices_pulse[0]
|
||||
drop_row = self.pnoe_df.loc[drop_idx]
|
||||
vo2_pulse_drop_bpm = int(drop_row["HR(bpm)_smoothed"])
|
||||
# Determine zone based on HR zones
|
||||
if pnoe_metrics.get("zone1_bpm") and vo2_pulse_drop_bpm:
|
||||
zones = [pnoe_metrics.get(f"zone{i}_bpm", "") for i in range(1, 6)]
|
||||
for i, zone_str in enumerate(zones, 1):
|
||||
if zone_str:
|
||||
zone_clean = zone_str.replace("bpm", "").strip()
|
||||
if "-" in zone_clean:
|
||||
parts = zone_clean.split("-")
|
||||
if len(parts) == 2:
|
||||
try:
|
||||
start, end = int(parts[0]), int(parts[1].replace("+", ""))
|
||||
if start <= vo2_pulse_drop_bpm <= end:
|
||||
vo2_pulse_drop_zone = f"Zone {i}"
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
elif "+" in zone_clean:
|
||||
# Zone 5 format: "180+bpm"
|
||||
try:
|
||||
start = int(zone_clean.replace("+", ""))
|
||||
if vo2_pulse_drop_bpm >= start:
|
||||
vo2_pulse_drop_zone = f"Zone {i}"
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Calculate slope of VO2 Breath
|
||||
vo2_breath_slope = self.pnoe_df["VO2 Breath_smoothed"].diff()
|
||||
vo2_breath_slope_smoothed = vo2_breath_slope.rolling(window=window, min_periods=1).mean()
|
||||
|
||||
# Find where VO2 Breath begins to drop
|
||||
mask_breath = vo2_breath_slope_smoothed <= 0
|
||||
drop_indices_breath = mask_breath[mask_breath].index
|
||||
|
||||
vo2_breath_drop_bpm = None
|
||||
vo2_breath_drop_zone = None
|
||||
if len(drop_indices_breath) > 0:
|
||||
drop_idx = drop_indices_breath[0]
|
||||
drop_row = self.pnoe_df.loc[drop_idx]
|
||||
vo2_breath_drop_bpm = int(drop_row["HR(bpm)_smoothed"])
|
||||
# Determine zone
|
||||
if pnoe_metrics.get("zone1_bpm") and vo2_breath_drop_bpm:
|
||||
zones = [pnoe_metrics.get(f"zone{i}_bpm", "") for i in range(1, 6)]
|
||||
for i, zone_str in enumerate(zones, 1):
|
||||
if zone_str:
|
||||
zone_clean = zone_str.replace("bpm", "").strip()
|
||||
if "-" in zone_clean:
|
||||
parts = zone_clean.split("-")
|
||||
if len(parts) == 2:
|
||||
try:
|
||||
start, end = int(parts[0]), int(parts[1].replace("+", ""))
|
||||
if start <= vo2_breath_drop_bpm <= end:
|
||||
vo2_breath_drop_zone = f"Zone {i}"
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
elif "+" in zone_clean:
|
||||
# Zone 5 format: "180+bpm"
|
||||
try:
|
||||
start = int(zone_clean.replace("+", ""))
|
||||
if vo2_breath_drop_bpm >= start:
|
||||
vo2_breath_drop_zone = f"Zone {i}"
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"vo2_pulse_drop_bpm": vo2_pulse_drop_bpm or 180,
|
||||
"vo2_pulse_drop_zone": vo2_pulse_drop_zone or "Zone 4",
|
||||
"vo2_breath_drop_bpm": vo2_breath_drop_bpm or 173,
|
||||
"vo2_breath_drop_zone": vo2_breath_drop_zone or "Zone 3",
|
||||
}
|
||||
|
||||
def _calculate_fat_metabolism_metrics(self, pnoe_metrics: Dict) -> Dict:
|
||||
"""Calculate fat metabolism metrics for page 11"""
|
||||
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
|
||||
fat_max_row = self.pnoe_df.loc[fat_max_idx]
|
||||
|
||||
fat_max_value = pnoe_metrics.get("fat_max_value", 0)
|
||||
fat_max_hr = pnoe_metrics.get("fat_max_hr", 0)
|
||||
max_hr = 220 - self.patient_info["age"]
|
||||
fat_max_heart_rate_pct = (fat_max_hr / max_hr * 100) if max_hr > 0 else 0
|
||||
|
||||
# Find carbs and fat crossover point
|
||||
crossover_idx = None
|
||||
for idx in self.pnoe_df.index:
|
||||
if self.pnoe_df.loc[idx, "CHO_smoothed"] > self.pnoe_df.loc[idx, "FAT_smoothed"]:
|
||||
crossover_idx = idx
|
||||
break
|
||||
|
||||
crossover_bpm = None
|
||||
crossover_heart_rate_pct = None
|
||||
if crossover_idx is not None:
|
||||
crossover_row = self.pnoe_df.loc[crossover_idx]
|
||||
crossover_bpm = int(crossover_row["HR(bpm)_smoothed"])
|
||||
crossover_heart_rate_pct = (crossover_bpm / max_hr * 100) if max_hr > 0 else 0
|
||||
|
||||
# Get speed and incline at fat max
|
||||
fat_max_speed = fat_max_row.get("Speed", 0)
|
||||
fat_max_incline = fat_max_row.get("Incline", 2.0) if "Incline" in fat_max_row else 2.0
|
||||
|
||||
return {
|
||||
"fat_max_value": f"{fat_max_value:.2f}Kcals/min",
|
||||
"fat_max_heart_rate": f"{fat_max_heart_rate_pct:.0f}% of Max Heart Rate",
|
||||
"fat_max_bpm": f"{int(fat_max_hr)} bpm",
|
||||
"fat_max_optimal": "*Optimal 10-12Kcals/minute",
|
||||
"crossover_bpm": f"{crossover_bpm or 100}bpm",
|
||||
"crossover_heart_rate": f"{crossover_heart_rate_pct or 51:.0f}% of Max Heart Rate",
|
||||
"fat_metabolism_note": f"{crossover_bpm or 100}bpm at a speed of {fat_max_speed:.1f}mph and incline of {fat_max_incline:.0f}%",
|
||||
}
|
||||
|
||||
def _calculate_recovery_metrics(self) -> Dict:
|
||||
"""Calculate recovery metrics for page 11"""
|
||||
# Find peak exercise point (max HR)
|
||||
peak_idx = self.pnoe_df["HR(bpm)_smoothed"].idxmax()
|
||||
peak_hr = self.pnoe_df.loc[peak_idx, "HR(bpm)_smoothed"]
|
||||
peak_time = self.pnoe_df.loc[peak_idx, "T(sec)"]
|
||||
|
||||
# Find recovery phase (after peak)
|
||||
recovery_df = self.pnoe_df[self.pnoe_df["T(sec)"] > peak_time].copy()
|
||||
|
||||
if len(recovery_df) == 0:
|
||||
return {
|
||||
"cardiac_recovery_time": "(1 minute)",
|
||||
"cardiac_recovery_percentage": "33%",
|
||||
"metabolic_recovery_time": "(2 minute)",
|
||||
"metabolic_recovery_percentage": "65%",
|
||||
"breath_recovery_time": "(2.5 minute)",
|
||||
"breath_recovery_percentage": "76%",
|
||||
}
|
||||
|
||||
# Cardiac recovery (1 minute)
|
||||
one_min_time = peak_time + 60
|
||||
one_min_row = recovery_df[recovery_df["T(sec)"] <= one_min_time]
|
||||
if len(one_min_row) > 0:
|
||||
one_min_hr = one_min_row.iloc[-1]["HR(bpm)_smoothed"]
|
||||
cardiac_recovery_pct = ((peak_hr - one_min_hr) / peak_hr * 100) if peak_hr > 0 else 0
|
||||
else:
|
||||
cardiac_recovery_pct = 33
|
||||
|
||||
# Metabolic recovery (2 minutes) - using VCO2
|
||||
two_min_time = peak_time + 120
|
||||
peak_vco2 = self.pnoe_df.loc[peak_idx, "VCO2(ml/min)_smoothed"]
|
||||
two_min_row = recovery_df[recovery_df["T(sec)"] <= two_min_time]
|
||||
if len(two_min_row) > 0:
|
||||
two_min_vco2 = two_min_row.iloc[-1]["VCO2(ml/min)_smoothed"]
|
||||
metabolic_recovery_pct = ((peak_vco2 - two_min_vco2) / peak_vco2 * 100) if peak_vco2 > 0 else 0
|
||||
else:
|
||||
metabolic_recovery_pct = 65
|
||||
|
||||
# Breath frequency recovery (2.5 minutes)
|
||||
two_five_min_time = peak_time + 150
|
||||
peak_bf = self.pnoe_df.loc[peak_idx, "BF(bpm)_smoothed"]
|
||||
two_five_min_row = recovery_df[recovery_df["T(sec)"] <= two_five_min_time]
|
||||
if len(two_five_min_row) > 0:
|
||||
two_five_min_bf = two_five_min_row.iloc[-1]["BF(bpm)_smoothed"]
|
||||
breath_recovery_pct = ((peak_bf - two_five_min_bf) / peak_bf * 100) if peak_bf > 0 else 0
|
||||
else:
|
||||
breath_recovery_pct = 76
|
||||
|
||||
return {
|
||||
"cardiac_recovery_time": "(1 minute)",
|
||||
"cardiac_recovery_percentage": f"{int(cardiac_recovery_pct)}%",
|
||||
"metabolic_recovery_time": "(2 minute)",
|
||||
"metabolic_recovery_percentage": f"{int(metabolic_recovery_pct)}%",
|
||||
"breath_recovery_time": "(2.5 minute)",
|
||||
"breath_recovery_percentage": f"{int(breath_recovery_pct)}%",
|
||||
}
|
||||
|
||||
def _calculate_resting_heart_rate_metrics(self) -> Dict:
|
||||
"""Calculate resting heart rate metrics for page 11"""
|
||||
# Get resting HR from beginning of test
|
||||
rest_phase = self.pnoe_df.head(30) # First 30 seconds
|
||||
resting_hr = rest_phase["HR(bpm)_smoothed"].mean()
|
||||
|
||||
age = self.patient_info.get("age", 30)
|
||||
gender = self.patient_info.get("gender", "female").lower()
|
||||
|
||||
# Determine age range
|
||||
if 26 <= age <= 35:
|
||||
age_range = "26-35"
|
||||
elif 36 <= age <= 45:
|
||||
age_range = "36-45"
|
||||
elif 46 <= age <= 55:
|
||||
age_range = "46-55"
|
||||
else:
|
||||
age_range = "26-35" # Default
|
||||
|
||||
# HR ranges based on gender and age (simplified)
|
||||
if gender == "female":
|
||||
hr_ranges = {
|
||||
"poor": "82bpm +",
|
||||
"below_avg": "75-81bpm",
|
||||
"average": "71-74bpm",
|
||||
"above_avg": "66-70bpm",
|
||||
"good": "62-65bpm",
|
||||
"excellent": "55-61bpm",
|
||||
"athlete": "44-54bpm",
|
||||
}
|
||||
else: # male
|
||||
hr_ranges = {
|
||||
"poor": "82bpm +",
|
||||
"below_avg": "75-81bpm",
|
||||
"average": "71-74bpm",
|
||||
"above_avg": "66-70bpm",
|
||||
"good": "62-65bpm",
|
||||
"excellent": "55-61bpm",
|
||||
"athlete": "44-54bpm",
|
||||
}
|
||||
|
||||
return {
|
||||
"resting_heart_rate": f"{int(resting_hr)}bpm",
|
||||
"hr_age_range": age_range,
|
||||
"hr_poor": hr_ranges["poor"],
|
||||
"hr_below_avg": hr_ranges["below_avg"],
|
||||
"hr_average": hr_ranges["average"],
|
||||
"hr_above_avg": hr_ranges["above_avg"],
|
||||
"hr_good": hr_ranges["good"],
|
||||
"hr_excellent": hr_ranges["excellent"],
|
||||
"hr_athlete": hr_ranges["athlete"],
|
||||
}
|
||||
|
||||
def calculate_rmr_and_fuel_source(self) -> Dict:
|
||||
"""Calculate RMR and fuel source from pnoe data"""
|
||||
metrics = {}
|
||||
|
||||
# Calculate RMR from resting phase (MET <= 1.1)
|
||||
if "MET" in self.pnoe_df.columns and "EE(kcal/day)" in self.pnoe_df.columns:
|
||||
rest_phase = self.pnoe_df[self.pnoe_df["MET"] <= 1.1]
|
||||
if not rest_phase.empty:
|
||||
rmr = rest_phase["EE(kcal/day)"].mean()
|
||||
metrics["rmr_kcal"] = float(rmr)
|
||||
else:
|
||||
# Fallback: use minimum EE(kcal/min) * 1440 (minutes per day)
|
||||
if "EE(kcal/min)" in self.pnoe_df.columns:
|
||||
min_ee = self.pnoe_df["EE(kcal/min)"].min()
|
||||
metrics["rmr_kcal"] = float(min_ee * 1440)
|
||||
else:
|
||||
metrics["rmr_kcal"] = 1500.0 # Default fallback
|
||||
else:
|
||||
# Fallback: estimate from weight (simplified)
|
||||
weight_kg = self.patient_info.get("weight", 70)
|
||||
gender = self.patient_info.get("gender", "female").lower()
|
||||
|
||||
# Simplified RMR estimation: 22 kcal/kg/day for men, 20 for women
|
||||
if gender == "male":
|
||||
rmr = weight_kg * 22
|
||||
else:
|
||||
rmr = weight_kg * 20
|
||||
metrics["rmr_kcal"] = float(rmr)
|
||||
|
||||
# Calculate fuel source from resting phase (RER == 0.9 or closest)
|
||||
if "RER" in self.pnoe_df.columns and "FAT(%)" in self.pnoe_df.columns:
|
||||
# Find rest phase with RER closest to 0.9
|
||||
rest_phase = (
|
||||
self.pnoe_df[self.pnoe_df["MET"] <= 1.1].copy()
|
||||
if "MET" in self.pnoe_df.columns
|
||||
else self.pnoe_df.copy()
|
||||
)
|
||||
if not rest_phase.empty:
|
||||
# Find row with RER closest to 0.9
|
||||
if "RER" in rest_phase.columns:
|
||||
rest_phase["RER_diff"] = abs(rest_phase["RER"] - 0.9)
|
||||
closest_idx = rest_phase["RER_diff"].idxmin()
|
||||
fat_pct = rest_phase.loc[closest_idx, "FAT(%)"]
|
||||
metrics["rest_fat_percentage"] = float(fat_pct)
|
||||
else:
|
||||
# Use mean FAT(%) from rest phase
|
||||
metrics["rest_fat_percentage"] = float(rest_phase["FAT(%)"].mean())
|
||||
else:
|
||||
# Fallback: use overall mean
|
||||
metrics["rest_fat_percentage"] = float(self.pnoe_df["FAT(%)"].mean())
|
||||
else:
|
||||
# Fallback: use a default value
|
||||
metrics["rest_fat_percentage"] = 75.0
|
||||
|
||||
# Calculate caloric values for page 5
|
||||
rmr = metrics["rmr_kcal"]
|
||||
neat = rmr * 0.25 # NEAT is typically 20-30% of RMR
|
||||
weight_loss_rate = 1.0 # 1 lb per week
|
||||
weight_loss_calories = 500.0 # 500 kcal deficit per day for 1 lb/week
|
||||
total_calories = rmr + neat - weight_loss_calories
|
||||
|
||||
metrics["resting_calories"] = int(rmr)
|
||||
metrics["neat_calories"] = int(neat)
|
||||
metrics["weight_loss_calories"] = int(weight_loss_calories)
|
||||
metrics["weight_loss_rate"] = weight_loss_rate
|
||||
metrics["total_calories"] = int(total_calories)
|
||||
|
||||
return metrics
|
||||
|
||||
def generate_all_contexts(
|
||||
self, patient_name: str, graphs: Dict[str, str], metric_overrides: Optional[Dict] = None
|
||||
) -> List[Dict]:
|
||||
"""Main method to generate all page contexts"""
|
||||
self,
|
||||
patient_name: str,
|
||||
graphs: Dict[str, str],
|
||||
metric_overrides: Optional[Dict] = None,
|
||||
) -> Dict[str, Dict]:
|
||||
"""Main method to generate all page contexts
|
||||
|
||||
Returns:
|
||||
Dictionary with keys 'page_1', 'page_2', etc., each containing context data for that page
|
||||
"""
|
||||
if metric_overrides is None:
|
||||
metric_overrides = {}
|
||||
|
||||
|
||||
self.extract_patient_info(patient_name)
|
||||
|
||||
|
||||
# Extract metric overrides for spirometry and pnoe
|
||||
spirometry_overrides = metric_overrides.get("spirometry", {})
|
||||
pnoe_overrides = metric_overrides.get("pnoe", {})
|
||||
|
||||
|
||||
spirometry_metrics = self.calculate_spirometry_metrics(spirometry_overrides)
|
||||
pnoe_metrics = self.calculate_pnoe_metrics(pnoe_overrides)
|
||||
rmr_metrics = self.calculate_rmr_and_fuel_source()
|
||||
|
||||
contexts = []
|
||||
contexts.append(
|
||||
{
|
||||
"name": self.patient_info["name"],
|
||||
"surname": self.patient_info["last_name"],
|
||||
"date": datetime.now().strftime("%B %d, %Y"),
|
||||
}
|
||||
)
|
||||
contexts.append(
|
||||
{
|
||||
contexts = {}
|
||||
|
||||
# Page 1
|
||||
contexts["page_1"] = {
|
||||
"name": self.patient_info["name"],
|
||||
"surname": self.patient_info["last_name"],
|
||||
"date": datetime.now().strftime("%B %d, %Y"),
|
||||
}
|
||||
|
||||
# Page 2
|
||||
contexts["page_2"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"test_date": datetime.now().strftime("%B %d, %Y"),
|
||||
}
|
||||
|
||||
# Pages 3, 6 (pages 4 and 5 are handled separately)
|
||||
for i in [0, 3]: # Skip indices 1 and 2 which are pages 4 and 5
|
||||
contexts[f"page_{i + 3}"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"test_date": datetime.now().strftime("%B %d, %Y"),
|
||||
"page_number": i + 3,
|
||||
}
|
||||
)
|
||||
|
||||
for i in range(4):
|
||||
contexts.append(
|
||||
{"patient_name": self.patient_info["name"], "page_number": i + 3}
|
||||
)
|
||||
# Page 4 - Nutrition Guidelines with Body Composition
|
||||
contexts["page_4"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 4,
|
||||
"fat_percentage": f"{self.patient_info['fat_percentage']:.1f}",
|
||||
"body_composition_chart": graphs.get("body_composition", ""),
|
||||
"body_fat_chart": graphs.get("body_fat_percent", ""), # Alias for template
|
||||
"body_fat_percent_chart": graphs.get(
|
||||
"body_fat_percent", ""
|
||||
), # Keep for consistency
|
||||
}
|
||||
|
||||
# Page 5 - Resting Metabolic Rate Assessment
|
||||
contexts["page_5"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 5,
|
||||
"metabolism_chart": graphs.get("metabolism_chart", ""),
|
||||
"fuel_source_chart": graphs.get("fuel_source_chart", ""),
|
||||
"resting_calories": rmr_metrics.get("resting_calories", 1500),
|
||||
"neat_calories": rmr_metrics.get("neat_calories", 375),
|
||||
"weight_loss_calories": rmr_metrics.get("weight_loss_calories", 500),
|
||||
"weight_loss_rate": rmr_metrics.get("weight_loss_rate", 1.0),
|
||||
"total_calories": rmr_metrics.get("total_calories", 1375),
|
||||
}
|
||||
|
||||
# Calculate FEV1 percentage for page 7
|
||||
fev1_percentage = 0
|
||||
if spirometry_metrics.get("fvc_best"):
|
||||
fev1_percentage = (
|
||||
pnoe_metrics["peak_vt"] / spirometry_metrics["fvc_best"]
|
||||
) * 100
|
||||
|
||||
contexts.append(
|
||||
{
|
||||
"peak_vt": f"{pnoe_metrics['peak_vt']:.2f}",
|
||||
"peak_vt_bpm": f"{int(pnoe_metrics['peak_vt_hr'])}",
|
||||
"fev1_percentage": f"{fev1_percentage:.1f}",
|
||||
"lung_analysis_chart": graphs.get("spirometry_chart", ""),
|
||||
"respiratory_analysis_chart": graphs.get("respiratory", ""),
|
||||
}
|
||||
)
|
||||
contexts.append(
|
||||
{
|
||||
"vo2_max_value": f"{pnoe_metrics['vo2_max_per_kg']:.1f}",
|
||||
"age_range": f"{self.patient_info['age'] // 10 * 10}-{self.patient_info['age'] // 10 * 10 + 9}",
|
||||
"zone1_bpm": pnoe_metrics.get("zone1_bpm", ""),
|
||||
"zone2_bpm": pnoe_metrics.get("zone2_bpm", ""),
|
||||
"zone3_bpm": pnoe_metrics.get("zone3_bpm", ""),
|
||||
"zone4_bpm": pnoe_metrics.get("zone4_bpm", ""),
|
||||
"zone5_bpm": pnoe_metrics.get("zone5_bpm", ""),
|
||||
"vo2_pulse_chart": graphs.get("vo2_pulse", ""),
|
||||
}
|
||||
)
|
||||
contexts.append(
|
||||
{
|
||||
"fat_max_value": f"{pnoe_metrics['fat_max_value']:.2f}",
|
||||
"fat_max_hr": f"{int(pnoe_metrics['fat_max_hr'])}",
|
||||
"fuel_utilization_chart": graphs.get("fuel_utilization", ""),
|
||||
"fat_metabolism_chart": graphs.get("fat_metabolism", ""),
|
||||
}
|
||||
)
|
||||
contexts.append(
|
||||
{
|
||||
"fat_percentage": f"{self.patient_info['fat_percentage']:.1f}",
|
||||
"fat_mass_lbs": f"{self.patient_info['fat_mass_lbs']:.1f}",
|
||||
"lean_mass_lbs": f"{self.patient_info['lean_mass_lbs']:.1f}",
|
||||
"body_composition_chart": graphs.get("body_composition", ""),
|
||||
"body_fat_percent_chart": graphs.get("body_fat_percent", ""),
|
||||
}
|
||||
)
|
||||
# Page 7
|
||||
contexts["page_7"] = {
|
||||
"peak_vt": f"{pnoe_metrics['peak_vt']:.2f}",
|
||||
"peak_vt_bpm": f"{int(pnoe_metrics['peak_vt_hr'])}",
|
||||
"fev1_percentage": f"{fev1_percentage:.1f}",
|
||||
"lung_analysis_chart": graphs.get("spirometry_chart", ""),
|
||||
"respiratory_analysis_chart": graphs.get("respiratory", ""),
|
||||
}
|
||||
|
||||
for i in range(9):
|
||||
contexts.append(
|
||||
{
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": i + 11,
|
||||
"vo2_breath_chart": graphs.get("vo2_breath", ""),
|
||||
"recovery_chart": graphs.get("recovery", ""),
|
||||
}
|
||||
)
|
||||
# Page 8
|
||||
contexts["page_8"] = {
|
||||
"vo2_max_value": f"{pnoe_metrics['vo2_max_per_kg']:.1f}",
|
||||
"age_range": f"{self.patient_info['age'] // 10 * 10}-{self.patient_info['age'] // 10 * 10 + 9}",
|
||||
"zone1_bpm": pnoe_metrics.get("zone1_bpm", ""),
|
||||
"zone2_bpm": pnoe_metrics.get("zone2_bpm", ""),
|
||||
"zone3_bpm": pnoe_metrics.get("zone3_bpm", ""),
|
||||
"zone4_bpm": pnoe_metrics.get("zone4_bpm", ""),
|
||||
"zone5_bpm": pnoe_metrics.get("zone5_bpm", ""),
|
||||
"vo2_pulse_chart": graphs.get("vo2_pulse", ""),
|
||||
}
|
||||
|
||||
# Page 9
|
||||
contexts["page_9"] = {
|
||||
"fat_max_value": f"{pnoe_metrics['fat_max_value']:.2f}",
|
||||
"fat_max_hr": f"{int(pnoe_metrics['fat_max_hr'])}",
|
||||
"fuel_utilization_chart": graphs.get("fuel_utilization", ""),
|
||||
"fat_metabolism_chart": graphs.get("fat_metabolism", ""),
|
||||
}
|
||||
|
||||
# Page 10 - VO2 Pulse and VO2 Breath
|
||||
vo2_drop_metrics = self._calculate_vo2_drop_points(pnoe_metrics)
|
||||
contexts["page_10"] = {
|
||||
"vo2_pulse_chart": graphs.get("vo2_pulse", ""),
|
||||
"vo2_breath_chart": graphs.get("vo2_breath", ""),
|
||||
"vo2_pulse_drop_bpm": f"{vo2_drop_metrics['vo2_pulse_drop_bpm']} bpm",
|
||||
"vo2_pulse_drop_zone": vo2_drop_metrics["vo2_pulse_drop_zone"],
|
||||
"vo2_breath_drop_bpm": f"{vo2_drop_metrics['vo2_breath_drop_bpm']} bpm",
|
||||
"vo2_breath_drop_zone": vo2_drop_metrics["vo2_breath_drop_zone"],
|
||||
}
|
||||
|
||||
# Page 11 - Fat Metabolism and Recovery
|
||||
fat_metabolism_metrics = self._calculate_fat_metabolism_metrics(pnoe_metrics)
|
||||
recovery_metrics = self._calculate_recovery_metrics()
|
||||
resting_hr_metrics = self._calculate_resting_heart_rate_metrics()
|
||||
|
||||
contexts["page_11"] = {
|
||||
"fat_metabolism_chart": graphs.get("fat_metabolism", ""),
|
||||
"recovery_chart": graphs.get("recovery", ""),
|
||||
**fat_metabolism_metrics,
|
||||
**recovery_metrics,
|
||||
**resting_hr_metrics,
|
||||
}
|
||||
|
||||
# Pages 12-17
|
||||
for i in range(6):
|
||||
contexts[f"page_{i + 12}"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": i + 12,
|
||||
}
|
||||
|
||||
# Page 18 - Glossary with Body Fat Percentage Chart
|
||||
contexts["page_18"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 18,
|
||||
"body_fat_percentage_chart": graphs.get("body_fat_percent", ""),
|
||||
}
|
||||
|
||||
# Page 19
|
||||
contexts["page_19"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"page_number": 19,
|
||||
}
|
||||
|
||||
return contexts
|
||||
|
||||
Reference in New Issue
Block a user