feat: Remove deprecated body fat percentage chart and integrate master chart for report generation
- Deleted the old body fat percentage chart image. - Updated report generation to load the new body fat percentage master chart for improved accuracy and consistency. - Refactored context generation to reference the new chart in the report structure.
This commit is contained in:
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -295,12 +295,14 @@ class ContextGenerator:
|
|||||||
# Calculate slope of VO2 Pulse
|
# Calculate slope of VO2 Pulse
|
||||||
vo2_pulse_slope = self.pnoe_df["VO2 Pulse_smoothed"].diff()
|
vo2_pulse_slope = self.pnoe_df["VO2 Pulse_smoothed"].diff()
|
||||||
window = max(1, len(self.pnoe_df) // 3) # Ensure window is at least 1
|
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()
|
vo2_pulse_slope_smoothed = vo2_pulse_slope.rolling(
|
||||||
|
window=window, min_periods=1
|
||||||
|
).mean()
|
||||||
|
|
||||||
# Find where VO2 Pulse begins to drop (slope becomes negative)
|
# Find where VO2 Pulse begins to drop (slope becomes negative)
|
||||||
mask_pulse = vo2_pulse_slope_smoothed <= 0
|
mask_pulse = vo2_pulse_slope_smoothed <= 0
|
||||||
drop_indices_pulse = mask_pulse[mask_pulse].index
|
drop_indices_pulse = mask_pulse[mask_pulse].index
|
||||||
|
|
||||||
vo2_pulse_drop_bpm = None
|
vo2_pulse_drop_bpm = None
|
||||||
vo2_pulse_drop_zone = None
|
vo2_pulse_drop_zone = None
|
||||||
if len(drop_indices_pulse) > 0:
|
if len(drop_indices_pulse) > 0:
|
||||||
@@ -317,7 +319,10 @@ class ContextGenerator:
|
|||||||
parts = zone_clean.split("-")
|
parts = zone_clean.split("-")
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
try:
|
try:
|
||||||
start, end = int(parts[0]), int(parts[1].replace("+", ""))
|
start, end = (
|
||||||
|
int(parts[0]),
|
||||||
|
int(parts[1].replace("+", "")),
|
||||||
|
)
|
||||||
if start <= vo2_pulse_drop_bpm <= end:
|
if start <= vo2_pulse_drop_bpm <= end:
|
||||||
vo2_pulse_drop_zone = f"Zone {i}"
|
vo2_pulse_drop_zone = f"Zone {i}"
|
||||||
break
|
break
|
||||||
@@ -332,15 +337,17 @@ class ContextGenerator:
|
|||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Calculate slope of VO2 Breath
|
# Calculate slope of VO2 Breath
|
||||||
vo2_breath_slope = self.pnoe_df["VO2 Breath_smoothed"].diff()
|
vo2_breath_slope = self.pnoe_df["VO2 Breath_smoothed"].diff()
|
||||||
vo2_breath_slope_smoothed = vo2_breath_slope.rolling(window=window, min_periods=1).mean()
|
vo2_breath_slope_smoothed = vo2_breath_slope.rolling(
|
||||||
|
window=window, min_periods=1
|
||||||
|
).mean()
|
||||||
|
|
||||||
# Find where VO2 Breath begins to drop
|
# Find where VO2 Breath begins to drop
|
||||||
mask_breath = vo2_breath_slope_smoothed <= 0
|
mask_breath = vo2_breath_slope_smoothed <= 0
|
||||||
drop_indices_breath = mask_breath[mask_breath].index
|
drop_indices_breath = mask_breath[mask_breath].index
|
||||||
|
|
||||||
vo2_breath_drop_bpm = None
|
vo2_breath_drop_bpm = None
|
||||||
vo2_breath_drop_zone = None
|
vo2_breath_drop_zone = None
|
||||||
if len(drop_indices_breath) > 0:
|
if len(drop_indices_breath) > 0:
|
||||||
@@ -357,7 +364,10 @@ class ContextGenerator:
|
|||||||
parts = zone_clean.split("-")
|
parts = zone_clean.split("-")
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
try:
|
try:
|
||||||
start, end = int(parts[0]), int(parts[1].replace("+", ""))
|
start, end = (
|
||||||
|
int(parts[0]),
|
||||||
|
int(parts[1].replace("+", "")),
|
||||||
|
)
|
||||||
if start <= vo2_breath_drop_bpm <= end:
|
if start <= vo2_breath_drop_bpm <= end:
|
||||||
vo2_breath_drop_zone = f"Zone {i}"
|
vo2_breath_drop_zone = f"Zone {i}"
|
||||||
break
|
break
|
||||||
@@ -372,7 +382,7 @@ class ContextGenerator:
|
|||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"vo2_pulse_drop_bpm": vo2_pulse_drop_bpm or 180,
|
"vo2_pulse_drop_bpm": vo2_pulse_drop_bpm or 180,
|
||||||
"vo2_pulse_drop_zone": vo2_pulse_drop_zone or "Zone 4",
|
"vo2_pulse_drop_zone": vo2_pulse_drop_zone or "Zone 4",
|
||||||
@@ -384,30 +394,37 @@ class ContextGenerator:
|
|||||||
"""Calculate fat metabolism metrics for page 11"""
|
"""Calculate fat metabolism metrics for page 11"""
|
||||||
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
|
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
|
||||||
fat_max_row = self.pnoe_df.loc[fat_max_idx]
|
fat_max_row = self.pnoe_df.loc[fat_max_idx]
|
||||||
|
|
||||||
fat_max_value = pnoe_metrics.get("fat_max_value", 0)
|
fat_max_value = pnoe_metrics.get("fat_max_value", 0)
|
||||||
fat_max_hr = pnoe_metrics.get("fat_max_hr", 0)
|
fat_max_hr = pnoe_metrics.get("fat_max_hr", 0)
|
||||||
max_hr = 220 - self.patient_info["age"]
|
max_hr = 220 - self.patient_info["age"]
|
||||||
fat_max_heart_rate_pct = (fat_max_hr / max_hr * 100) if max_hr > 0 else 0
|
fat_max_heart_rate_pct = (fat_max_hr / max_hr * 100) if max_hr > 0 else 0
|
||||||
|
|
||||||
# Find carbs and fat crossover point
|
# Find carbs and fat crossover point
|
||||||
crossover_idx = None
|
crossover_idx = None
|
||||||
for idx in self.pnoe_df.index:
|
for idx in self.pnoe_df.index:
|
||||||
if self.pnoe_df.loc[idx, "CHO_smoothed"] > self.pnoe_df.loc[idx, "FAT_smoothed"]:
|
if (
|
||||||
|
self.pnoe_df.loc[idx, "CHO_smoothed"]
|
||||||
|
> self.pnoe_df.loc[idx, "FAT_smoothed"]
|
||||||
|
):
|
||||||
crossover_idx = idx
|
crossover_idx = idx
|
||||||
break
|
break
|
||||||
|
|
||||||
crossover_bpm = None
|
crossover_bpm = None
|
||||||
crossover_heart_rate_pct = None
|
crossover_heart_rate_pct = None
|
||||||
if crossover_idx is not None:
|
if crossover_idx is not None:
|
||||||
crossover_row = self.pnoe_df.loc[crossover_idx]
|
crossover_row = self.pnoe_df.loc[crossover_idx]
|
||||||
crossover_bpm = int(crossover_row["HR(bpm)_smoothed"])
|
crossover_bpm = int(crossover_row["HR(bpm)_smoothed"])
|
||||||
crossover_heart_rate_pct = (crossover_bpm / max_hr * 100) if max_hr > 0 else 0
|
crossover_heart_rate_pct = (
|
||||||
|
(crossover_bpm / max_hr * 100) if max_hr > 0 else 0
|
||||||
|
)
|
||||||
|
|
||||||
# Get speed and incline at fat max
|
# Get speed and incline at fat max
|
||||||
fat_max_speed = fat_max_row.get("Speed", 0)
|
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
|
fat_max_incline = (
|
||||||
|
fat_max_row.get("Incline", 2.0) if "Incline" in fat_max_row else 2.0
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"fat_max_value": f"{fat_max_value:.2f}Kcals/min",
|
"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_heart_rate": f"{fat_max_heart_rate_pct:.0f}% of Max Heart Rate",
|
||||||
@@ -424,10 +441,10 @@ class ContextGenerator:
|
|||||||
peak_idx = self.pnoe_df["HR(bpm)_smoothed"].idxmax()
|
peak_idx = self.pnoe_df["HR(bpm)_smoothed"].idxmax()
|
||||||
peak_hr = self.pnoe_df.loc[peak_idx, "HR(bpm)_smoothed"]
|
peak_hr = self.pnoe_df.loc[peak_idx, "HR(bpm)_smoothed"]
|
||||||
peak_time = self.pnoe_df.loc[peak_idx, "T(sec)"]
|
peak_time = self.pnoe_df.loc[peak_idx, "T(sec)"]
|
||||||
|
|
||||||
# Find recovery phase (after peak)
|
# Find recovery phase (after peak)
|
||||||
recovery_df = self.pnoe_df[self.pnoe_df["T(sec)"] > peak_time].copy()
|
recovery_df = self.pnoe_df[self.pnoe_df["T(sec)"] > peak_time].copy()
|
||||||
|
|
||||||
if len(recovery_df) == 0:
|
if len(recovery_df) == 0:
|
||||||
return {
|
return {
|
||||||
"cardiac_recovery_time": "(1 minute)",
|
"cardiac_recovery_time": "(1 minute)",
|
||||||
@@ -437,36 +454,42 @@ class ContextGenerator:
|
|||||||
"breath_recovery_time": "(2.5 minute)",
|
"breath_recovery_time": "(2.5 minute)",
|
||||||
"breath_recovery_percentage": "76%",
|
"breath_recovery_percentage": "76%",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cardiac recovery (1 minute)
|
# Cardiac recovery (1 minute)
|
||||||
one_min_time = peak_time + 60
|
one_min_time = peak_time + 60
|
||||||
one_min_row = recovery_df[recovery_df["T(sec)"] <= one_min_time]
|
one_min_row = recovery_df[recovery_df["T(sec)"] <= one_min_time]
|
||||||
if len(one_min_row) > 0:
|
if len(one_min_row) > 0:
|
||||||
one_min_hr = one_min_row.iloc[-1]["HR(bpm)_smoothed"]
|
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
|
cardiac_recovery_pct = (
|
||||||
|
((peak_hr - one_min_hr) / peak_hr * 100) if peak_hr > 0 else 0
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
cardiac_recovery_pct = 33
|
cardiac_recovery_pct = 33
|
||||||
|
|
||||||
# Metabolic recovery (2 minutes) - using VCO2
|
# Metabolic recovery (2 minutes) - using VCO2
|
||||||
two_min_time = peak_time + 120
|
two_min_time = peak_time + 120
|
||||||
peak_vco2 = self.pnoe_df.loc[peak_idx, "VCO2(ml/min)_smoothed"]
|
peak_vco2 = self.pnoe_df.loc[peak_idx, "VCO2(ml/min)_smoothed"]
|
||||||
two_min_row = recovery_df[recovery_df["T(sec)"] <= two_min_time]
|
two_min_row = recovery_df[recovery_df["T(sec)"] <= two_min_time]
|
||||||
if len(two_min_row) > 0:
|
if len(two_min_row) > 0:
|
||||||
two_min_vco2 = two_min_row.iloc[-1]["VCO2(ml/min)_smoothed"]
|
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
|
metabolic_recovery_pct = (
|
||||||
|
((peak_vco2 - two_min_vco2) / peak_vco2 * 100) if peak_vco2 > 0 else 0
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
metabolic_recovery_pct = 65
|
metabolic_recovery_pct = 65
|
||||||
|
|
||||||
# Breath frequency recovery (2.5 minutes)
|
# Breath frequency recovery (2.5 minutes)
|
||||||
two_five_min_time = peak_time + 150
|
two_five_min_time = peak_time + 150
|
||||||
peak_bf = self.pnoe_df.loc[peak_idx, "BF(bpm)_smoothed"]
|
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]
|
two_five_min_row = recovery_df[recovery_df["T(sec)"] <= two_five_min_time]
|
||||||
if len(two_five_min_row) > 0:
|
if len(two_five_min_row) > 0:
|
||||||
two_five_min_bf = two_five_min_row.iloc[-1]["BF(bpm)_smoothed"]
|
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
|
breath_recovery_pct = (
|
||||||
|
((peak_bf - two_five_min_bf) / peak_bf * 100) if peak_bf > 0 else 0
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
breath_recovery_pct = 76
|
breath_recovery_pct = 76
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"cardiac_recovery_time": "(1 minute)",
|
"cardiac_recovery_time": "(1 minute)",
|
||||||
"cardiac_recovery_percentage": f"{int(cardiac_recovery_pct)}%",
|
"cardiac_recovery_percentage": f"{int(cardiac_recovery_pct)}%",
|
||||||
@@ -481,10 +504,10 @@ class ContextGenerator:
|
|||||||
# Get resting HR from beginning of test
|
# Get resting HR from beginning of test
|
||||||
rest_phase = self.pnoe_df.head(30) # First 30 seconds
|
rest_phase = self.pnoe_df.head(30) # First 30 seconds
|
||||||
resting_hr = rest_phase["HR(bpm)_smoothed"].mean()
|
resting_hr = rest_phase["HR(bpm)_smoothed"].mean()
|
||||||
|
|
||||||
age = self.patient_info.get("age", 30)
|
age = self.patient_info.get("age", 30)
|
||||||
gender = self.patient_info.get("gender", "female").lower()
|
gender = self.patient_info.get("gender", "female").lower()
|
||||||
|
|
||||||
# Determine age range
|
# Determine age range
|
||||||
if 26 <= age <= 35:
|
if 26 <= age <= 35:
|
||||||
age_range = "26-35"
|
age_range = "26-35"
|
||||||
@@ -494,7 +517,7 @@ class ContextGenerator:
|
|||||||
age_range = "46-55"
|
age_range = "46-55"
|
||||||
else:
|
else:
|
||||||
age_range = "26-35" # Default
|
age_range = "26-35" # Default
|
||||||
|
|
||||||
# HR ranges based on gender and age (simplified)
|
# HR ranges based on gender and age (simplified)
|
||||||
if gender == "female":
|
if gender == "female":
|
||||||
hr_ranges = {
|
hr_ranges = {
|
||||||
@@ -516,7 +539,7 @@ class ContextGenerator:
|
|||||||
"excellent": "55-61bpm",
|
"excellent": "55-61bpm",
|
||||||
"athlete": "44-54bpm",
|
"athlete": "44-54bpm",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"resting_heart_rate": f"{int(resting_hr)}bpm",
|
"resting_heart_rate": f"{int(resting_hr)}bpm",
|
||||||
"hr_age_range": age_range,
|
"hr_age_range": age_range,
|
||||||
@@ -562,8 +585,8 @@ class ContextGenerator:
|
|||||||
if "RER" in self.pnoe_df.columns and "FAT(%)" in self.pnoe_df.columns:
|
if "RER" in self.pnoe_df.columns and "FAT(%)" in self.pnoe_df.columns:
|
||||||
# Find rest phase with RER closest to 0.9
|
# Find rest phase with RER closest to 0.9
|
||||||
rest_phase = (
|
rest_phase = (
|
||||||
self.pnoe_df[self.pnoe_df["MET"] <= 1.1].copy()
|
self.pnoe_df[self.pnoe_df["RER"] == 0.9].copy()
|
||||||
if "MET" in self.pnoe_df.columns
|
if "RER" in self.pnoe_df.columns
|
||||||
else self.pnoe_df.copy()
|
else self.pnoe_df.copy()
|
||||||
)
|
)
|
||||||
if not rest_phase.empty:
|
if not rest_phase.empty:
|
||||||
@@ -720,7 +743,7 @@ class ContextGenerator:
|
|||||||
fat_metabolism_metrics = self._calculate_fat_metabolism_metrics(pnoe_metrics)
|
fat_metabolism_metrics = self._calculate_fat_metabolism_metrics(pnoe_metrics)
|
||||||
recovery_metrics = self._calculate_recovery_metrics()
|
recovery_metrics = self._calculate_recovery_metrics()
|
||||||
resting_hr_metrics = self._calculate_resting_heart_rate_metrics()
|
resting_hr_metrics = self._calculate_resting_heart_rate_metrics()
|
||||||
|
|
||||||
contexts["page_11"] = {
|
contexts["page_11"] = {
|
||||||
"fat_metabolism_chart": graphs.get("fat_metabolism", ""),
|
"fat_metabolism_chart": graphs.get("fat_metabolism", ""),
|
||||||
"recovery_chart": graphs.get("recovery", ""),
|
"recovery_chart": graphs.get("recovery", ""),
|
||||||
@@ -735,14 +758,16 @@ class ContextGenerator:
|
|||||||
"patient_name": self.patient_info["name"],
|
"patient_name": self.patient_info["name"],
|
||||||
"page_number": i + 12,
|
"page_number": i + 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Page 18 - Glossary with Body Fat Percentage Chart
|
# Page 18 - Glossary with Body Fat Percentage Master Chart
|
||||||
contexts["page_18"] = {
|
contexts["page_18"] = {
|
||||||
"patient_name": self.patient_info["name"],
|
"patient_name": self.patient_info["name"],
|
||||||
"page_number": 18,
|
"page_number": 18,
|
||||||
"body_fat_percentage_chart": graphs.get("body_fat_percent", ""),
|
"body_fat_percentage_chart": graphs.get(
|
||||||
|
"body_fat_percentage_master_chart", ""
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Page 19
|
# Page 19
|
||||||
contexts["page_19"] = {
|
contexts["page_19"] = {
|
||||||
"patient_name": self.patient_info["name"],
|
"patient_name": self.patient_info["name"],
|
||||||
|
|||||||
@@ -404,6 +404,23 @@ class ReportGeneratorService:
|
|||||||
print(f"Warning: Could not generate body fat percent chart: {e}")
|
print(f"Warning: Could not generate body fat percent chart: {e}")
|
||||||
graphs_dict["body_fat_percent"] = ""
|
graphs_dict["body_fat_percent"] = ""
|
||||||
|
|
||||||
|
# Load static body fat percentage master chart for page 18
|
||||||
|
master_chart_path = Path("app/body_fat_percentage_master_chart.png")
|
||||||
|
if master_chart_path.exists():
|
||||||
|
try:
|
||||||
|
with open(master_chart_path, "rb") as f:
|
||||||
|
graphs_dict["body_fat_percentage_master_chart"] = base64.b64encode(
|
||||||
|
f.read()
|
||||||
|
).decode("utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not load body fat percentage master chart: {e}")
|
||||||
|
graphs_dict["body_fat_percentage_master_chart"] = ""
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Warning: Body fat percentage master chart not found at {master_chart_path}"
|
||||||
|
)
|
||||||
|
graphs_dict["body_fat_percentage_master_chart"] = ""
|
||||||
|
|
||||||
# Generate spirometry chart
|
# Generate spirometry chart
|
||||||
print("Step 4: Generating spirometry chart...")
|
print("Step 4: Generating spirometry chart...")
|
||||||
try:
|
try:
|
||||||
@@ -419,6 +436,7 @@ class ReportGeneratorService:
|
|||||||
print("Spirometry chart generated successfully")
|
print("Spirometry chart generated successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
error_details = traceback.format_exc()
|
error_details = traceback.format_exc()
|
||||||
print(f"Warning: Could not generate spirometry chart: {e}")
|
print(f"Warning: Could not generate spirometry chart: {e}")
|
||||||
print(f"Error details: {error_details}")
|
print(f"Error details: {error_details}")
|
||||||
|
|||||||
Reference in New Issue
Block a user