Checkpoint 3
This commit is contained in:
@@ -232,10 +232,15 @@ class ContextGenerator:
|
||||
if zone_key in metric_overrides:
|
||||
metrics[zone_key] = metric_overrides[zone_key]
|
||||
else:
|
||||
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
|
||||
fat_max_row = self.pnoe_df.loc[fat_max_idx]
|
||||
# Use optimal fat burning zone (highest fat:carb ratio) - same as _calculate_zone_metrics
|
||||
# This ensures consistency between zone calculations and zone metrics
|
||||
self.pnoe_df["fat_carb_ratio"] = self.pnoe_df["FAT_smoothed"] / (
|
||||
self.pnoe_df["CHO_smoothed"] + 0.00000001
|
||||
)
|
||||
optimal_fat_idx = self.pnoe_df["fat_carb_ratio"].idxmax()
|
||||
optimal_row = self.pnoe_df.loc[optimal_fat_idx]
|
||||
zones = self._calculate_hr_zones(
|
||||
metrics["vt1"], metrics["vt2"], fat_max_row
|
||||
metrics["vt1"], metrics["vt2"], optimal_row
|
||||
)
|
||||
metrics.update(zones)
|
||||
|
||||
@@ -280,29 +285,46 @@ class ContextGenerator:
|
||||
return vt1, vt2
|
||||
|
||||
def _calculate_hr_zones(
|
||||
self, vt1: Optional[Dict], vt2: Optional[Dict], fat_max_row: pd.Series
|
||||
self, vt1: Optional[Dict], vt2: Optional[Dict], optimal_row: pd.Series
|
||||
) -> Dict:
|
||||
"""Calculate heart rate zones based on thresholds"""
|
||||
"""Calculate heart rate zones based on thresholds
|
||||
|
||||
Uses optimal fat burning zone (highest fat:carb ratio) to match _calculate_zone_metrics.
|
||||
This ensures consistency between zone string calculations and zone metrics table.
|
||||
"""
|
||||
import math
|
||||
|
||||
zones = {}
|
||||
if vt1 and vt2:
|
||||
zone_1_start = fat_max_row["HR(bpm)_smoothed"] - 15
|
||||
zone_2_start = fat_max_row["HR(bpm)_smoothed"]
|
||||
zone_3_start = vt1["HeartRate"]
|
||||
zone_4_start = vt2["HeartRate"] - 10
|
||||
zone_5_start = vt2["HeartRate"] + 10
|
||||
# Use same zone boundary calculation as _calculate_zone_metrics
|
||||
zone_1_start = math.floor(optimal_row["HR(bpm)_smoothed"] - 15)
|
||||
zone_2_start = math.floor(optimal_row["HR(bpm)_smoothed"])
|
||||
zone_3_start = math.floor(vt1["HeartRate"])
|
||||
zone_4_start = math.floor(vt2["HeartRate"] - 10)
|
||||
zone_5_start = math.floor(vt2["HeartRate"])
|
||||
# zone_5_end is calculated for consistency with _calculate_zone_metrics
|
||||
# (not used in string format since zone 5 is open-ended: "+bpm")
|
||||
zone_5_end = math.floor(vt2["HeartRate"] + 10) # noqa: F841
|
||||
|
||||
zones["zone1_bpm"] = f"{int(zone_1_start)}-{int(zone_2_start)}bpm"
|
||||
zones["zone2_bpm"] = f"{int(zone_2_start)}-{int(vt1['HeartRate'])}bpm"
|
||||
zones["zone3_bpm"] = f"{int(zone_3_start)}-{int(zone_4_start)}bpm"
|
||||
zones["zone4_bpm"] = f"{int(zone_4_start)}-{int(zone_5_start)}bpm"
|
||||
zones["zone5_bpm"] = f"{int(zone_5_start)}+bpm"
|
||||
# Calculate zone ends to match _calculate_zone_metrics exactly
|
||||
zone_1_end = zone_2_start
|
||||
zone_2_end = math.floor(vt1["HeartRate"])
|
||||
zone_3_end = zone_4_start
|
||||
zone_4_end = zone_5_start
|
||||
|
||||
# Format zones to match _calculate_zone_metrics output
|
||||
zones["zone1_bpm"] = f"{int(zone_1_start)}-{int(zone_1_end)}bpm"
|
||||
zones["zone2_bpm"] = f"{int(zone_2_start)}-{int(zone_2_end)}bpm"
|
||||
zones["zone3_bpm"] = f"{int(zone_3_start)}-{int(zone_3_end)}bpm"
|
||||
zones["zone4_bpm"] = f"{int(zone_4_start)}-{int(zone_4_end)}bpm"
|
||||
zones["zone5_bpm"] = f"{int(zone_5_start)}-{int(zone_5_end)}bpm"
|
||||
else:
|
||||
max_hr = 220 - self.patient_info["age"]
|
||||
zones["zone1_bpm"] = f"{int(max_hr * 0.55)}-{int(max_hr * 0.65)}bpm"
|
||||
zones["zone2_bpm"] = f"{int(max_hr * 0.65)}-{int(max_hr * 0.75)}bpm"
|
||||
zones["zone3_bpm"] = f"{int(max_hr * 0.75)}-{int(max_hr * 0.85)}bpm"
|
||||
zones["zone4_bpm"] = f"{int(max_hr * 0.85)}-{int(max_hr * 0.95)}bpm"
|
||||
zones["zone5_bpm"] = f"{int(max_hr * 0.95)}+bpm"
|
||||
zones["zone5_bpm"] = f"{int(max_hr * 0.95)}-{int(max_hr * 1.05)}bpm"
|
||||
return zones
|
||||
|
||||
def _calculate_vo2_drop_points(self, pnoe_metrics: Dict) -> Dict:
|
||||
@@ -1180,7 +1202,9 @@ class ContextGenerator:
|
||||
"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_chart": graphs.get(
|
||||
"body_fat_percent", ""
|
||||
), # Alias for template
|
||||
"body_fat_percent_chart": graphs.get(
|
||||
"body_fat_percent", ""
|
||||
), # Keep for consistency
|
||||
@@ -1199,29 +1223,29 @@ class ContextGenerator:
|
||||
"weight_loss_rate": rmr_metrics.get("weight_loss_rate", 1.0),
|
||||
"total_calories": rmr_metrics.get("total_calories", 1375),
|
||||
}
|
||||
|
||||
|
||||
# For minimal reports, also generate resting heart rate table for page_5
|
||||
if report_type == "minimal" and graph_generator:
|
||||
resting_hr_metrics = self._calculate_resting_heart_rate_metrics()
|
||||
rhr_table_info = self._calculate_rhr_table_data(
|
||||
self.patient_info["age"], self.patient_info["gender"]
|
||||
)
|
||||
|
||||
|
||||
# Get resting heart rate value and determine category
|
||||
rhr_value_str = resting_hr_metrics.get("resting_heart_rate", "0bpm")
|
||||
rhr_value = float(rhr_value_str.replace("bpm", "").strip())
|
||||
|
||||
|
||||
category = self._determine_rhr_category(
|
||||
rhr_value,
|
||||
self.patient_info["age"],
|
||||
self.patient_info["gender"],
|
||||
)
|
||||
|
||||
|
||||
gender_label = (
|
||||
"F" if self.patient_info["gender"].lower().startswith("f") else "M"
|
||||
)
|
||||
age_range_label = f"{rhr_table_info['age_range']} ({gender_label})"
|
||||
|
||||
|
||||
rhr_columns = [
|
||||
"Age",
|
||||
"Poor",
|
||||
@@ -1244,7 +1268,7 @@ class ContextGenerator:
|
||||
rhr_table_info["ranges"]["Athlete"],
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
contexts["page_5"]["rhr_table"] = (
|
||||
graph_generator.generate_resting_heart_rate_table(
|
||||
data=rhr_data,
|
||||
@@ -1265,12 +1289,16 @@ class ContextGenerator:
|
||||
"deficit_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 0.39 / 4)}g Carbs",
|
||||
"deficit_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 0.39 / 9)}g Fat",
|
||||
"deficit_fiber": "24g Fibre",
|
||||
"refeed_weekday_calories": int(rmr_metrics.get("total_calories", 1600) * 0.85),
|
||||
"refeed_weekday_calories": int(
|
||||
rmr_metrics.get("total_calories", 1600) * 0.85
|
||||
),
|
||||
"refeed_weekday_protein": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.22 / 4)}g Protein",
|
||||
"refeed_weekday_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.39 / 4)}g Carbs",
|
||||
"refeed_weekday_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.39 / 9)}g Fat",
|
||||
"refeed_weekday_fiber": "20g Fibre",
|
||||
"refeed_weekend_calories": int(rmr_metrics.get("total_calories", 1600) * 1.375),
|
||||
"refeed_weekend_calories": int(
|
||||
rmr_metrics.get("total_calories", 1600) * 1.375
|
||||
),
|
||||
"refeed_weekend_protein": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.22 / 4)}g Protein",
|
||||
"refeed_weekend_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.39 / 4)}g Carbs",
|
||||
"refeed_weekend_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.39 / 9)}g Fat",
|
||||
@@ -1291,12 +1319,12 @@ class ContextGenerator:
|
||||
|
||||
# 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", ""),
|
||||
}
|
||||
"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", ""),
|
||||
}
|
||||
|
||||
# Page 8
|
||||
contexts["page_8"] = {
|
||||
@@ -1562,7 +1590,11 @@ class ContextGenerator:
|
||||
}
|
||||
|
||||
# For minimal reports, create combined context for page_19_20_minimal
|
||||
if report_type == "minimal" and 19 in pages_to_generate and 20 in pages_to_generate:
|
||||
if (
|
||||
report_type == "minimal"
|
||||
and 19 in pages_to_generate
|
||||
and 20 in pages_to_generate
|
||||
):
|
||||
contexts["page_19_20_minimal"] = {
|
||||
"patient_name": self.patient_info["name"],
|
||||
"body_fat_percentage_chart": graphs.get(
|
||||
|
||||
Reference in New Issue
Block a user