diff --git a/app/__pycache__/database.cpython-312.pyc b/app/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..236c321 Binary files /dev/null and b/app/__pycache__/database.cpython-312.pyc differ diff --git a/app/__pycache__/session_manager.cpython-312.pyc b/app/__pycache__/session_manager.cpython-312.pyc new file mode 100644 index 0000000..a96370f Binary files /dev/null and b/app/__pycache__/session_manager.cpython-312.pyc differ diff --git a/app/main.py b/app/main.py index a9c9e8a..3f9dec1 100644 --- a/app/main.py +++ b/app/main.py @@ -179,10 +179,10 @@ async def upload_files( # Prepare patient information patient_name = f"{first_name} {last_name}" print(f"DEBUG: Received next_testing_date: '{next_testing_date}'") - + # Generate session_id internally using timestamp for unique identification session_id = datetime.now().strftime("%Y%m%d_%H%M%S") - + patient_info = { "patient_name": patient_name, "first_name": first_name, @@ -642,7 +642,7 @@ async def generate_report( # Generate session_id internally using timestamp for unique identification session_id = datetime.now().strftime("%Y%m%d_%H%M%S") - + # Prepare patient information patient_info = { "patient_name": patient_name, diff --git a/app/report_gen/page_2.html b/app/report_gen/page_2.html index 581b88f..1092793 100644 --- a/app/report_gen/page_2.html +++ b/app/report_gen/page_2.html @@ -15,7 +15,8 @@
3
@@ -35,7 +36,8 @@
4
@@ -52,7 +54,8 @@
5
@@ -66,7 +69,8 @@
9
@@ -80,7 +84,8 @@
10
@@ -94,7 +99,8 @@
12
@@ -111,7 +117,8 @@
13
diff --git a/app/report_gen/page_2_minimal.html b/app/report_gen/page_2_minimal.html index eb21fe1..fb22cc2 100644 --- a/app/report_gen/page_2_minimal.html +++ b/app/report_gen/page_2_minimal.html @@ -15,7 +15,8 @@
3
@@ -35,7 +36,8 @@
4
@@ -49,7 +51,8 @@
5
@@ -66,7 +69,8 @@
6
@@ -82,8 +86,3 @@
- - - - - diff --git a/app/report_gen/page_7.html b/app/report_gen/page_7.html index 556cf35..7703a2f 100644 --- a/app/report_gen/page_7.html +++ b/app/report_gen/page_7.html @@ -26,7 +26,7 @@

Indications

-

{{ indication | default('No Respiratory Capacity Limitations')}}

+

{{ indication | default('No Respiratory Capacity Limitation')}}

diff --git a/app/services/__pycache__/context_generator.cpython-312.pyc b/app/services/__pycache__/context_generator.cpython-312.pyc index 80bbd60..ca6d4e2 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 2508cf6..61d96ed 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/context_generator.py b/app/services/context_generator.py index d261260..1b5bf5a 100644 --- a/app/services/context_generator.py +++ b/app/services/context_generator.py @@ -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( diff --git a/notebooks/analysis.ipynb b/notebooks/analysis.ipynb index 37eff10..d51e5c2 100644 --- a/notebooks/analysis.ipynb +++ b/notebooks/analysis.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "b18c1027", "metadata": {}, "outputs": [], @@ -88,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "56a9d655", "metadata": {}, "outputs": [ @@ -104,7 +104,10 @@ ], "source": [ "import pandas as pd\n", - "spirometry_df = pd.read_csv(\"data/spirometry_data.csv\")\n", + "import os\n", + "\n", + "base_dir = os.path.dirname(os.path.abspath('.'))\n", + "spirometry_df = pd.read_csv(f\"{base_dir}/data/spirometry_data.csv\")\n", "\n", "fvc_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', 'Best'].values[0]\n", "fvc_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', '%Pred.'].values[0]\n", @@ -122,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "990f4b4f", "metadata": {}, "outputs": [ @@ -136,7 +139,7 @@ } ], "source": [ - "df = pd.read_csv('data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n", + "df = pd.read_csv(f'{base_dir}/data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n", "peak_vt = df['VT(l)'].max()\n", "max_vt_row = df.loc[df['VT(l)'].idxmax()]\n", "print(f\"Peak VT: {peak_vt}\")\n", @@ -146,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "041cbc3d", "metadata": {}, "outputs": [ @@ -154,21 +157,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "Peak VT: 2.3770000000000002\n", - "HR at Peak VT: 171.525\n" + "Peak VT: 2.3844444444444446\n", + "HR at Peak VT: 172.80555555555554\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "/tmp/ipykernel_69398/4157056299.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n", + "/tmp/ipykernel_53922/361246798.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n", " df = df.apply(pd.to_numeric, errors='ignore')\n" ] } ], "source": [ - "df = pd.read_csv('data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n", + "df = pd.read_csv(f'{base_dir}/data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n", "# Convert all columns to numeric where possible, coercing errors to NaN\n", "df = df.apply(pd.to_numeric, errors='ignore')\n", "df['VO2 Pulse'] = df['VO2(ml/min)'] / df['HR(bpm)'] # VO2 Pulse in mL/beat\n", @@ -176,7 +179,7 @@ "df['CHO'] = df['EE(kcal/min)'] * df['CARBS(%)']/100\n", "df['FAT'] = df['EE(kcal/min)'] * df['FAT(%)']/100\n", "# Smooth key columns using rolling window\n", - "window_size = 10\n", + "window_size = 9\n", "\n", "# List of columns to smooth\n", "columns_to_smooth = ['VO2(ml/min)', 'VCO2(ml/min)', 'HR(bpm)', 'VT(l)', 'BF(bpm)', 'VE(l/min)', 'VO2 Pulse', 'VO2 Breath', 'CHO', 'FAT']\n", @@ -195,7 +198,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "de7cadd1", "metadata": {}, "outputs": [ @@ -203,7 +206,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Percent FEV: 72.91411042944786\n" + "Percent FEV: 73.14246762099523\n" ] } ], @@ -214,7 +217,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "cb972ed3", "metadata": {}, "outputs": [ @@ -311,13 +314,13 @@ "[1 rows x 147 columns]" ] }, - "execution_count": 11, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "personal_df = pd.read_excel('data/SECA body comp for all patients.xlsx')\n", + "personal_df = pd.read_excel(f'{base_dir}/data/SECA body comp for all patients.xlsx')\n", "\n", "keirstyn_data = personal_df[personal_df['LastName'].str.contains('Moran', case=False, na=False)]\n", "keirstyn_data" @@ -325,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "98d9295a", "metadata": {}, "outputs": [ @@ -333,7 +336,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "VO2 Max: 47.906290322580645\n" + "VO2 Max: 48.19062126642772\n" ] } ], @@ -823,7 +826,7 @@ ], "metadata": { "kernelspec": { - "display_name": "report_generation", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -837,7 +840,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.12.6" } }, "nbformat": 4,