diff --git a/app/body_fat_percentage_chart.png b/app/body_fat_percentage_master_chart.png similarity index 100% rename from app/body_fat_percentage_chart.png rename to app/body_fat_percentage_master_chart.png diff --git a/app/services/__pycache__/context_generator.cpython-312.pyc b/app/services/__pycache__/context_generator.cpython-312.pyc index e3208d4..7171c8b 100644 Binary files a/app/services/__pycache__/context_generator.cpython-312.pyc and b/app/services/__pycache__/context_generator.cpython-312.pyc differ diff --git a/app/services/__pycache__/graph_generator.cpython-312.pyc b/app/services/__pycache__/graph_generator.cpython-312.pyc index cce5a2c..4dde181 100644 Binary files a/app/services/__pycache__/graph_generator.cpython-312.pyc and b/app/services/__pycache__/graph_generator.cpython-312.pyc differ diff --git a/app/services/__pycache__/report_generator.cpython-312.pyc b/app/services/__pycache__/report_generator.cpython-312.pyc index bfff0c2..141c3e6 100644 Binary files a/app/services/__pycache__/report_generator.cpython-312.pyc and b/app/services/__pycache__/report_generator.cpython-312.pyc differ diff --git a/app/services/context_generator.py b/app/services/context_generator.py index 4c3f563..6d9e1e4 100644 --- a/app/services/context_generator.py +++ b/app/services/context_generator.py @@ -295,12 +295,14 @@ class ContextGenerator: # 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() - + 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: @@ -317,7 +319,10 @@ class ContextGenerator: parts = zone_clean.split("-") if len(parts) == 2: 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: vo2_pulse_drop_zone = f"Zone {i}" break @@ -332,15 +337,17 @@ class ContextGenerator: 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() - + 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: @@ -357,7 +364,10 @@ class ContextGenerator: parts = zone_clean.split("-") if len(parts) == 2: 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: vo2_breath_drop_zone = f"Zone {i}" break @@ -372,7 +382,7 @@ class ContextGenerator: 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", @@ -384,30 +394,37 @@ class ContextGenerator: """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"]: + 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 - + 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 - + 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", @@ -424,10 +441,10 @@ class ContextGenerator: 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)", @@ -437,36 +454,42 @@ class ContextGenerator: "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 + 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 + 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 + 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)}%", @@ -481,10 +504,10 @@ class ContextGenerator: # 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" @@ -494,7 +517,7 @@ class ContextGenerator: age_range = "46-55" else: age_range = "26-35" # Default - + # HR ranges based on gender and age (simplified) if gender == "female": hr_ranges = { @@ -516,7 +539,7 @@ class ContextGenerator: "excellent": "55-61bpm", "athlete": "44-54bpm", } - + return { "resting_heart_rate": f"{int(resting_hr)}bpm", "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: # 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 + self.pnoe_df[self.pnoe_df["RER"] == 0.9].copy() + if "RER" in self.pnoe_df.columns else self.pnoe_df.copy() ) if not rest_phase.empty: @@ -720,7 +743,7 @@ class ContextGenerator: 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", ""), @@ -735,14 +758,16 @@ class ContextGenerator: "patient_name": self.patient_info["name"], "page_number": i + 12, } - - # Page 18 - Glossary with Body Fat Percentage Chart + + # Page 18 - Glossary with Body Fat Percentage Master Chart contexts["page_18"] = { "patient_name": self.patient_info["name"], "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 contexts["page_19"] = { "patient_name": self.patient_info["name"], diff --git a/app/services/report_generator.py b/app/services/report_generator.py index 56f37c5..1919421 100644 --- a/app/services/report_generator.py +++ b/app/services/report_generator.py @@ -404,6 +404,23 @@ class ReportGeneratorService: print(f"Warning: Could not generate body fat percent chart: {e}") 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 print("Step 4: Generating spirometry chart...") try: @@ -419,6 +436,7 @@ class ReportGeneratorService: print("Spirometry chart generated successfully") except Exception as e: import traceback + error_details = traceback.format_exc() print(f"Warning: Could not generate spirometry chart: {e}") print(f"Error details: {error_details}")