From 7e985c497e8c3fd8e82ebea29069b6ab4579795d Mon Sep 17 00:00:00 2001 From: bolade Date: Tue, 18 Nov 2025 16:57:39 +0100 Subject: [PATCH] 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. --- app/body_fat_percentage_chart.png | Bin 0 -> 92762 bytes app/main.py | 310 +++++++--- .../context_generator.cpython-312.pyc | Bin 16064 -> 30745 bytes .../graph_generator.cpython-312.pyc | Bin 35202 -> 46136 bytes .../report_generator.cpython-312.pyc | Bin 17295 -> 21250 bytes app/services/context_generator.py | 571 +++++++++++++++--- app/services/graph_generator.py | 415 ++++++++++++- app/services/report_generator.py | 177 ++++-- app/templates/base.html | 1 + app/templates/upload.html | 11 +- notebooks/graphs.ipynb | 21 +- notebooks/page_context_gen.ipynb | 12 +- 12 files changed, 1256 insertions(+), 262 deletions(-) create mode 100644 app/body_fat_percentage_chart.png diff --git a/app/body_fat_percentage_chart.png b/app/body_fat_percentage_chart.png new file mode 100644 index 0000000000000000000000000000000000000000..7e24de3f37d903f8047d9d6f5e085a5b40c69ec3 GIT binary patch literal 92762 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ąDg14wK$k3~>V7=8ZjrrgNvGru3D0g_JsW@d$Ii00n)~Es`w7cuaP+ zDFm*G4{%*HZ+W6EkEbJmVc$ABEi;i(KW20P*Alt;=UV?1bYNU**7;BG#J)6a| zK2dQj;G8GG#qv4#D>oC%^oq;Fo-2Bf~|K4Ra%T|ryS*+J;uJbM9Lf)g3$K8*+ zvGMSW^w5HAsI@Z5QS8^Y%vI=50&*EcdrmCmHiXK- zJ!6D3dQ;>9{?gNV+qp_=tVkCh`hD=hC!72gCY2mUCL_`5oj7+$P2@=Lm{HN!*3RNL zb*Kf7z~e1>OJefGW`*?~e)J2QP+foq$~zDL=ZV`{D``vdBL@jqi2DeTL;c;SMuWag zD5;H&9V$O@-q@IQGgLV(#f&d$OuN&}bFi%6TKImu^I7|4Bj-g05e{;9L&}Ozl0_Sg zX-X_C6furp?44McJWSoHBZZt0(Ez62abZ*%ecYeF-JL(R8!`h_J$ta`0Dxydooflk zGmu7G%vR9I<3;9mNMpI*yaDXa^cq!FXm+|VvO9TvlA^Es! zis1AMqSDP+I&uJ9V^YOplu@0h-HNTRdy_+4f5=m#M-h;JAUAFhUUL?#txrue1`Dw9 zyW1C9OyrlpWI>Dh^&mwC41E_gVr*4KWBoO>7=Q@rgADOk+_8Fx`%}Hjbqy=4i+KE~ zI2mDQI=SfXW0LdiXs=a~`X1Fad6+9TM)0AyDC9v@o~cV?lPX1`DY2~rJsvb5EdGJk zMV3yN((0ieEtgPnEiop_Fn$DlH%zpJ4KEf+sK9Y|A>ncOw@$&#fhJA$%FFm?q?%peB1l!C$IYl!UhI* zHrbCFxU%nHgioVl^7J^)x(jA+`2S35om|}JU&UUziP4&ucpsCg{;=Qty(+po81m+E zR@SGbiiH5}^dLhMN(WR{s^C%}@%b~VqsIs4@v(L`rne(dRc_{4{(N!P+tUs!DcE2% zC-rBCq-eS7Lu)&*i{qNZsV(T!%gmwTI6ZZO18O*O(e8moaZbN32|=eb3IHpG~z zP!Uo_8G|``#x(GS(}BI+Fjw}KsRYjm?G9eYS0u_e`{GxstFAP8`c_t^-&&i=cim?e z=oGll4>mU$-!0U|VN$j($1*F8$$X&H)8py{`#wGCNBf+yuiNbJ34Pm_%up$aEvWhg zihLhs#Vcm=zS~TSP(_`yP7%s^1^h$$TP?b8YYY+_#5_Ag;KXaELaWe|^3=CCBiU3z z**j0AP4yWsLWaMc`B%Eqc)?}6260jhyQS&euiNcyA zwD`Hk@9Z$#&dSzUnl6F1`(x z;3aYNaDe+_f-vd%me=EEzl#blYy8%5PP>{w7tYp+PSwb(JsAA6kXKa_OoM{xi&NrM zJvD;k6G(Cq+<~TaSQ%lKyT)9&-4S0>wuXm?yGKBZhqxbfF8Pd(FV9{nk(D#xPwEps z)P*u5bR+p9!2L5hAm7f);TxsOo#6V{>6f)!{S*Xc^v;f>o=k+<4@Rw%q-;;2DjGPg z^hwsPG_AP=Rjl&aUNA7^Fg8d!F_c0$-%bJ69wrURHS*HJ9reP^2%Jh= zx7vt57Z1>AM*Af>V)7{I8LvJx7 z-ycsuOmERpM=nwP%jfrA%jk*jLI_09CNQq{7YbWFWG)$l_}OUz6HI3CAty`AkPU@p1^)wI6UT98E&HLExda77UnngtQ&l6F|sh8wyY^mu^ zph-iE>ohe7a2QX6ZUKxLz8|HFu#DRf_?Y!Szm$khS=n>$ew()eXkht3r=~cTq)^Cv zy)*dD(cHRZ@L;-jRwuCD-rcR`s-TvyOMm*32J}ArQ8;MQYdJ+mR8TZ(Br`^pq3F<4 z)ur2-%GbOXd_gVu@zpgztZbldj%EsDJJBf*Z%%)d(ie(zCz*LB=1YOIe#!L{$M!C^ z?ayLmSQgOC7Kh2uwJzfQaAE^aZ^_X@GL`|Z{`n&s9ZrprFO?yB#>B3v#IgnD-xx9LAWOv!F3_FbshKiHpj#&= z2}GNFRH3hCuQDR+603KaD7jY2i~kg1wmXRmrRgj zxaeoSZvig;zIJ=L7MRGjfpM@R{!Vy;ww!{T<5)~w{5UhUYyZR_|LYkJna|xHv)OgR zwuxVfe<;H{$JL$T%xA!#@**aejQ$vCowGpU6J&t|7Qk>iBdn{~X@VC!CV>(U;@XG= z8EEHDl*X!waL^>bqQV^<&zAV&Ed8!(LD$l&vycWy+-91SI42#)*j8C)FkgU=Gj-)4 zjTt2GtGbLz*3+p)zc)R?_`kmso3O2z<|M=a4&gh#nilw^CKY3dB@2?aluC&1i*h`Z z7S2+~nW56n#y)DQ_6(rG=#uUDu7Ks$rh~`@rvds>VGYq3|L!uO_N(C+5y~ksi@~IS z6NJd)lOyz|t{1plm`O-My7tVE9gP+1oEpOpOI$J_$@#e?ul+@?RxQ_~X5r+=M`OZT zTl%5x2}1L!a(&%y62~JUmoo#8Kg>TYQs8{EW8?;0DF#LPpTnxOhYNjGX_3R4z>;y^ zJKBt5Y!%g&<2S8+O~~X0MQ|n@V}cbmOf!hXQ?NCEn=ey$TbSP4`+aSoBjcM!ryBkM zcw00>(v z>{}c6yGIG7cSkWyAqaL|)=Z(rtsNb#`u0H=!B7;Cks`i@kfi;#TA@Le4wak}OQgWE z)Khs+Q6W|8u~8>SebyH$V{-)=i9ghlsA}8O_ve?rBUW9aRQW=+RZaJ~OUhHpTk)

W1?iHpjwybrcIfA;fEgPiBUVpfc?dzv%)Kl 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 HTMLResponse: """Helper function to render Jinja2 templates""" template = jinja_env.get_template(template_name) html_content = template.render(**context) - return HTMLResponse(content=html_content) + return HTMLResponse(content=html_content, media_type="text/html") @app.get("/", response_class=HTMLResponse) async def root(request: Request): """Root endpoint - Upload form page""" - return render_template("upload.html", {"request": request, "session": request.session}) + return render_template( + "upload.html", {"request": request, "session": request.session} + ) @app.post("/upload") @@ -77,56 +107,73 @@ async def upload_files( height: str = Form(...), weight: str = Form(...), gender: str = Form(...), + fat_percentage: float = Form(...), focus: str = Form(default="Endurance"), session_id: str = Form(default="default"), spirometry_pdf: UploadFile = File(...), pnoe_csv: UploadFile = File(...), - seca_excel: UploadFile = File(...), + oxygenation_csv: UploadFile = File(None), ): """Handle file upload and generate report""" # Validate file types if not spirometry_pdf.filename.endswith(".pdf"): - return render_template("upload.html", { - "request": request, - "session": request.session, - "error": "Spirometry file must be a PDF" - }) - + return render_template( + "upload.html", + { + "request": request, + "session": request.session, + "error": "Spirometry file must be a PDF", + }, + ) + if not pnoe_csv.filename.endswith(".csv"): - return render_template("upload.html", { - "request": request, - "session": request.session, - "error": "Pnoe file must be a CSV" - }) - - if not seca_excel.filename.endswith((".xlsx", ".xls")): - return render_template("upload.html", { - "request": request, - "session": request.session, - "error": "SECA file must be an Excel file (.xlsx or .xls)" - }) - + return render_template( + "upload.html", + { + "request": request, + "session": request.session, + "error": "Pnoe file must be a CSV", + }, + ) + + # Validate oxygenation CSV if provided + if oxygenation_csv and oxygenation_csv.filename: + if not oxygenation_csv.filename.endswith(".csv"): + return render_template( + "upload.html", + { + "request": request, + "session": request.session, + "error": "Oxygenation file must be a CSV", + }, + ) + # Create session-specific temp directory session_uuid = str(uuid.uuid4()) session_temp_dir = TEMP_DIR / session_uuid session_temp_dir.mkdir(exist_ok=True, parents=True) - + # Save uploaded files spirometry_path = session_temp_dir / f"spirometry_{spirometry_pdf.filename}" pnoe_path = session_temp_dir / f"pnoe_{pnoe_csv.filename}" - seca_path = session_temp_dir / f"seca_{seca_excel.filename}" - + oxygenation_path = None + try: # Write files with open(spirometry_path, "wb") as f: shutil.copyfileobj(spirometry_pdf.file, f) - + with open(pnoe_path, "wb") as f: shutil.copyfileobj(pnoe_csv.file, f) - - with open(seca_path, "wb") as f: - shutil.copyfileobj(seca_excel.file, f) - + + # Save oxygenation CSV if provided + if oxygenation_csv and oxygenation_csv.filename: + oxygenation_path = ( + session_temp_dir / f"oxygenation_{oxygenation_csv.filename}" + ) + with open(oxygenation_path, "wb") as f: + shutil.copyfileobj(oxygenation_csv.file, f) + # Prepare patient information patient_name = f"{first_name} {last_name}" patient_info = { @@ -137,76 +184,100 @@ async def upload_files( "height": height, "weight": weight, "gender": gender, + "fat_percentage": fat_percentage, "focus": focus, "session_id": session_id, } - + # Generate report + oxygenation_csv_path = str(oxygenation_path) if oxygenation_path else None result = await report_service.generate_report( spirometry_pdf_path=str(spirometry_path), pnoe_csv_path=str(pnoe_path), - seca_excel_path=str(seca_path), patient_info=patient_info, + oxygenation_csv_path=oxygenation_csv_path, ) - + # Store in session request.session["patient_info"] = patient_info request.session["temp_dir"] = str(session_temp_dir) request.session["report_path"] = result["report_path"] request.session["graphs_generated"] = result["graphs_generated"] request.session["analysis_data"] = result["analysis_data"] - + # Extract spirometry CSV path (it's saved in data_dir by the service) - from services.spirometry_table_extractor import extract_spirometry_table_from_pdf - from services.context_generator import ContextGenerator from pathlib import Path as PathLib - + + from services.context_generator import ContextGenerator + from services.spirometry_table_extractor import ( + extract_spirometry_table_from_pdf, + ) + # The spirometry CSV is extracted during report generation # We need to find it or extract it again data_dir = PathLib("data") - spirometry_csv_path = data_dir / f"spirometry_{Path(spirometry_pdf.filename).stem}.csv" - + spirometry_csv_path = ( + data_dir / f"spirometry_{Path(spirometry_pdf.filename).stem}.csv" + ) + # If it doesn't exist, extract it if not spirometry_csv_path.exists(): spirometry_csv_path = extract_spirometry_table_from_pdf( str(spirometry_path), output_dir=str(data_dir) ) spirometry_csv_path = PathLib(spirometry_csv_path) - + # Get calculated metrics for display and editing context_gen = ContextGenerator() context_gen.load_data( str(pnoe_path), str(spirometry_csv_path), - str(seca_path) + None, # No SECA file needed anymore ) - context_gen.extract_patient_info(last_name) # Extract patient info + # Set patient info manually since we're not reading from SECA + weight_kg = float(weight.replace("lbs", "").replace("kg", "").strip()) + if "lbs" in weight.lower(): + weight_kg = weight_kg / 2.20462 # Convert lbs to kg + + context_gen.patient_info = { + "name": first_name, + "last_name": last_name, + "age": age, + "weight": weight_kg, + "fat_percentage": fat_percentage, + "gender": gender, + } spirometry_metrics = context_gen.calculate_spirometry_metrics() pnoe_metrics = context_gen.calculate_pnoe_metrics() - + # Store metrics in session request.session["metrics"] = { "spirometry": spirometry_metrics, "pnoe": pnoe_metrics, } request.session["spirometry_csv_path"] = str(spirometry_csv_path) - + return RedirectResponse(url="/preview", status_code=303) - + except Exception as e: import traceback + error_details = traceback.format_exc() print(f"ERROR: {error_details}") - return render_template("upload.html", { - "request": request, - "session": request.session, - "error": f"Error generating report: {str(e)}" - }) + return render_template( + "upload.html", + { + "request": request, + "session": request.session, + "error": f"Error generating report: {str(e)}", + }, + ) finally: # Close file handles spirometry_pdf.file.close() pnoe_csv.file.close() - seca_excel.file.close() + if oxygenation_csv and oxygenation_csv.filename: + oxygenation_csv.file.close() @app.get("/preview", response_class=HTMLResponse) @@ -214,7 +285,9 @@ async def preview(request: Request): """Preview generated report""" if not request.session.get("report_path"): return RedirectResponse(url="/", status_code=303) - return render_template("preview.html", {"request": request, "session": request.session}) + return render_template( + "preview.html", {"request": request, "session": request.session} + ) @app.get("/graphs/{filename}") @@ -231,7 +304,9 @@ async def edit_form(request: Request): """Display edit metrics form""" if not request.session.get("metrics"): return RedirectResponse(url="/", status_code=303) - return render_template("edit.html", {"request": request, "session": request.session}) + return render_template( + "edit.html", {"request": request, "session": request.session} + ) @app.post("/edit") @@ -239,16 +314,13 @@ async def edit_metrics(request: Request): """Handle metric edits and regenerate report""" if not request.session.get("temp_dir") or not request.session.get("patient_info"): return RedirectResponse(url="/", status_code=303) - + # Get form data form_data = await request.form() - + # Build metric overrides - metric_overrides = { - "pnoe": {}, - "spirometry": {} - } - + metric_overrides = {"pnoe": {}, "spirometry": {}} + # Pnoe overrides if form_data.get("vo2_max"): metric_overrides["pnoe"]["vo2_max"] = float(form_data["vo2_max"]) @@ -262,28 +334,36 @@ async def edit_metrics(request: Request): metric_overrides["pnoe"]["fat_max_value"] = float(form_data["fat_max_value"]) if form_data.get("fat_max_hr"): metric_overrides["pnoe"]["fat_max_hr"] = float(form_data["fat_max_hr"]) - + # VT1 and VT2 overrides - if form_data.get("vt1_hr") or form_data.get("vt1_speed") or form_data.get("vt1_time"): + if ( + form_data.get("vt1_hr") + or form_data.get("vt1_speed") + or form_data.get("vt1_time") + ): metric_overrides["pnoe"]["vt1"] = { "HeartRate": float(form_data.get("vt1_hr", 0)), "Speed": float(form_data.get("vt1_speed", 0)), - "Time": float(form_data.get("vt1_time", 0)) + "Time": float(form_data.get("vt1_time", 0)), } - - if form_data.get("vt2_hr") or form_data.get("vt2_speed") or form_data.get("vt2_time"): + + if ( + form_data.get("vt2_hr") + or form_data.get("vt2_speed") + or form_data.get("vt2_time") + ): metric_overrides["pnoe"]["vt2"] = { "HeartRate": float(form_data.get("vt2_hr", 0)), "Speed": float(form_data.get("vt2_speed", 0)), - "Time": float(form_data.get("vt2_time", 0)) + "Time": float(form_data.get("vt2_time", 0)), } - + # Heart rate zones for i in range(1, 6): zone_key = f"zone{i}_bpm" if form_data.get(zone_key): metric_overrides["pnoe"][zone_key] = form_data[zone_key] - + # Spirometry overrides if form_data.get("fvc_best"): metric_overrides["spirometry"]["fvc_best"] = float(form_data["fvc_best"]) @@ -294,88 +374,120 @@ async def edit_metrics(request: Request): if form_data.get("fev1_pred"): metric_overrides["spirometry"]["fev1_pred"] = float(form_data["fev1_pred"]) if form_data.get("fev1_fvc_pct_best"): - metric_overrides["spirometry"]["fev1_fvc_pct_best"] = float(form_data["fev1_fvc_pct_best"]) + metric_overrides["spirometry"]["fev1_fvc_pct_best"] = float( + form_data["fev1_fvc_pct_best"] + ) if form_data.get("fev1_fvc_pct_pred"): - metric_overrides["spirometry"]["fev1_fvc_pct_pred"] = float(form_data["fev1_fvc_pct_pred"]) - + metric_overrides["spirometry"]["fev1_fvc_pct_pred"] = float( + form_data["fev1_fvc_pct_pred"] + ) + try: # Get file paths from session temp_dir = Path(request.session["temp_dir"]) patient_info = request.session["patient_info"] - + # Find files in temp directory spirometry_path = None pnoe_path = None - seca_path = None - + oxygenation_path = None + for file_path in temp_dir.iterdir(): if file_path.name.startswith("spirometry_"): spirometry_path = file_path elif file_path.name.startswith("pnoe_"): pnoe_path = file_path - elif file_path.name.startswith("seca_"): - seca_path = file_path - - if not all([spirometry_path, pnoe_path, seca_path]): - raise ValueError("Could not find all uploaded files") - + elif file_path.name.startswith("oxygenation_"): + oxygenation_path = file_path + + if not all([spirometry_path, pnoe_path]): + raise ValueError("Could not find all required uploaded files") + # Regenerate report with overrides + oxygenation_csv_path = str(oxygenation_path) if oxygenation_path else None result = await report_service.generate_report( spirometry_pdf_path=str(spirometry_path), pnoe_csv_path=str(pnoe_path), - seca_excel_path=str(seca_path), patient_info=patient_info, - metric_overrides=metric_overrides if (metric_overrides["pnoe"] or metric_overrides["spirometry"]) else None, + metric_overrides=metric_overrides + if (metric_overrides["pnoe"] or metric_overrides["spirometry"]) + else None, + oxygenation_csv_path=oxygenation_csv_path, ) - + # Update session with new report request.session["report_path"] = result["report_path"] request.session["graphs_generated"] = result["graphs_generated"] request.session["analysis_data"] = result["analysis_data"] - + # Recalculate metrics with overrides from services.context_generator import ContextGenerator + context_gen = ContextGenerator() spirometry_csv_path = request.session.get("spirometry_csv_path", "") if not spirometry_csv_path or not Path(spirometry_csv_path).exists(): - from services.spirometry_table_extractor import extract_spirometry_table_from_pdf from pathlib import Path as PathLib + + from services.spirometry_table_extractor import ( + extract_spirometry_table_from_pdf, + ) + data_dir = PathLib("data") spirometry_csv_path = extract_spirometry_table_from_pdf( str(spirometry_path), output_dir=str(data_dir) ) spirometry_csv_path = str(PathLib(spirometry_csv_path)) - + context_gen.load_data( str(pnoe_path), spirometry_csv_path, - str(seca_path) + None, # No SECA file ) + # Set patient info manually + weight_str = patient_info.get("weight", "0") + weight_kg = float(weight_str.replace("lbs", "").replace("kg", "").strip()) + if "lbs" in weight_str.lower(): + weight_kg = weight_kg / 2.20462 # Convert lbs to kg + + context_gen.patient_info = { + "name": patient_info.get("first_name", ""), + "last_name": patient_info.get("last_name", ""), + "age": patient_info.get("age", 25), + "weight": weight_kg, + "fat_percentage": patient_info.get("fat_percentage", 0), + "gender": patient_info.get("gender", "female"), + } context_gen.extract_patient_info(patient_info.get("last_name", "")) - + spirometry_overrides = metric_overrides.get("spirometry", {}) pnoe_overrides = metric_overrides.get("pnoe", {}) - spirometry_metrics = context_gen.calculate_spirometry_metrics(spirometry_overrides) + spirometry_metrics = context_gen.calculate_spirometry_metrics( + spirometry_overrides + ) pnoe_metrics = context_gen.calculate_pnoe_metrics(pnoe_overrides) - + # Update metrics in session request.session["metrics"] = { "spirometry": spirometry_metrics, "pnoe": pnoe_metrics, } request.session["spirometry_csv_path"] = spirometry_csv_path - + return RedirectResponse(url="/preview", status_code=303) - + except Exception as e: import traceback + error_details = traceback.format_exc() print(f"ERROR: {error_details}") - return render_template("edit.html", { - "request": request, - "session": request.session, - "error": f"Error regenerating report: {str(e)}" - }) + return render_template( + "edit.html", + { + "request": request, + "session": request.session, + "error": f"Error regenerating report: {str(e)}", + }, + ) @app.get("/health") diff --git a/app/services/__pycache__/context_generator.cpython-312.pyc b/app/services/__pycache__/context_generator.cpython-312.pyc index 5576322fd1cdcf5e8841e3f6e407915fece55f81..e3208d4cb2c2c303cf93d20665bf5557c5c23878 100644 GIT binary patch literal 30745 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 QgyA#hBu%1bfl>7OUje;u$p8QV diff --git a/app/services/__pycache__/graph_generator.cpython-312.pyc b/app/services/__pycache__/graph_generator.cpython-312.pyc index 9c844136024df09599b338d0717d44ab10669909..cce5a2c9b5460d7d5ffa027bce396b935515d4d0 100644 GIT binary patch delta 15621 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 -0gcun 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 diff --git a/app/services/context_generator.py b/app/services/context_generator.py index afbd0b4..4c3f563 100644 --- a/app/services/context_generator.py +++ b/app/services/context_generator.py @@ -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 diff --git a/app/services/graph_generator.py b/app/services/graph_generator.py index 82e3454..d1f9620 100644 --- a/app/services/graph_generator.py +++ b/app/services/graph_generator.py @@ -584,6 +584,105 @@ class GraphGenerator: return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path) + def generate_tsi_chart( + self, oxygenation_df: pd.DataFrame, save_as_base64: bool = True + ) -> str: + """ + Generate TSI (Tissue Saturation Index) chart with trend lines per stage. + + Args: + oxygenation_df: DataFrame with Time, TSI, and TSI-second columns + save_as_base64: If True, return base64 string, else return file path + + Returns: + Base64 string or file path + """ + from numpy.polynomial.polynomial import Polynomial + + plt.figure(figsize=(12, 5.5)) + + # Plot TSI (Left Leg) + plt.plot( + oxygenation_df["Time"], + oxygenation_df["TSI"], + label="TSI (Left Leg)", + color="steelblue", + linewidth=2, + ) + + # Plot TSI2 (Right Leg) + plt.plot( + oxygenation_df["Time"], + oxygenation_df["TSI-second"], + label="TSI2 (Right Leg)", + color="orange", + linewidth=2, + ) + + # Define time intervals for stages (adjust these based on your test protocol) + max_time = oxygenation_df["Time"].max() + intervals = [ + (0, 250), + (250, 500), + (500, 750), + (750, 1000), + (1000, 1250), + (1250, 1500), + (1500, max_time), + ] + + # Calculate and plot trend lines for each interval + for start_time, end_time in intervals: + # Filter data for this interval + mask_interval = (oxygenation_df["Time"] >= start_time) & ( + oxygenation_df["Time"] <= end_time + ) + + # TSI (Left Leg) trend for this interval + mask_left = mask_interval & ~oxygenation_df["TSI"].isna() + if mask_left.sum() > 1: # Need at least 2 points for a line + x_left = oxygenation_df.loc[mask_left, "Time"] + y_left = oxygenation_df.loc[mask_left, "TSI"] + coefs_left = Polynomial.fit(x_left, y_left, 1).convert().coef + trend_left = coefs_left[0] + coefs_left[1] * x_left + plt.plot( + x_left, + trend_left, + color="black", + linestyle="--", + linewidth=2, + alpha=0.8, + ) + + # TSI-second (Right Leg) trend for this interval + mask_right = mask_interval & ~oxygenation_df["TSI-second"].isna() + if mask_right.sum() > 1: # Need at least 2 points for a line + x_right = oxygenation_df.loc[mask_right, "Time"] + y_right = oxygenation_df.loc[mask_right, "TSI-second"] + coefs_right = Polynomial.fit(x_right, y_right, 1).convert().coef + trend_right = coefs_right[0] + coefs_right[1] * x_right + plt.plot( + x_right, + trend_right, + color="black", + linestyle="--", + linewidth=2, + alpha=0.8, + ) + + plt.xlabel("Time (s)") + plt.ylabel("TSI (%)") + plt.title("TSI (Left) and TSI2 (Right) with Black Slope Lines per Stage") + plt.legend(fontsize=10, loc="upper right") + plt.grid(alpha=0.25) + plt.tight_layout() + + chart_path = self.charts_dir / "tsi_chart.png" + plt.savefig(chart_path, bbox_inches="tight", dpi=160) + plt.close() + + return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path) + def generate_body_composition_chart( self, fat_mass_lbs: float, lean_mass_lbs: float, save_as_base64: bool = True ) -> str: @@ -678,25 +777,52 @@ class GraphGenerator: else: age_group = "20-39" # Default - demographic = f"{age_group}\n({gender[0].upper()})" + gender_abbrev = "M" if gender.lower() == "male" else "F" + demographic = f"{age_group}\n({gender_abbrev})" - # Define segments based on gender (female example) + # Define segments based on gender and age group if gender.lower() == "female": - segments = [ - ("#F8A8A8", 0, 15), # Muted Red: 0% to 15% - ("#FFEECC", 15, 5), # Pale Yellow: 15% to 20% - ("#D0F0C0", 20, 15), # Pale Green: 20% to 35% - ("#FFEECC", 35, 5), # Pale Yellow: 35% to 40% - ("#F8A8A8", 40, 10), # Muted Red: 40% to 50% - ] + if age_group == "20-39": + segments = [ + ("#F8A8A8", 0, 15), # Bad: 0-15% + ("#FFEECC", 15, 5), # Okay: 15-20% + ("#D0F0C0", 20, 15), # Good: 20-35% + ("#FFEECC", 35, 5), # Okay: 35-40% + ("#F8A8A8", 40, 10), # Bad: 40-50% + ] + else: # 40-59 and 60-79 have same ranges for females + segments = [ + ("#F8A8A8", 0, 20), # Bad: 0-20% + ("#FFEECC", 20, 5), # Okay: 20-25% + ("#D0F0C0", 25, 10), # Good: 25-35% + ("#FFEECC", 35, 5), # Okay: 35-40% + ("#F8A8A8", 40, 10), # Bad: 40-50% + ] else: # male - segments = [ - ("#F8A8A8", 0, 5), # Muted Red: 0% to 5% - ("#FFEECC", 5, 5), # Pale Yellow: 5% to 10% - ("#D0F0C0", 10, 10), # Pale Green: 10% to 20% - ("#FFEECC", 20, 5), # Pale Yellow: 20% to 25% - ("#F8A8A8", 25, 25), # Muted Red: 25% to 50% - ] + if age_group == "20-39": + segments = [ + ("#F8A8A8", 0, 5), # Bad: 0-5% + ("#FFEECC", 5, 5), # Okay: 5-10% + ("#D0F0C0", 10, 10), # Good: 10-20% + ("#FFEECC", 20, 5), # Okay: 20-25% + ("#F8A8A8", 25, 25), # Bad: 25-50% + ] + elif age_group == "40-59": + segments = [ + ("#F8A8A8", 0, 5), # Bad: 0-5% + ("#FFEECC", 5, 5), # Okay: 5-10% + ("#D0F0C0", 10, 10), # Good: 10-20% + ("#FFEECC", 20, 10), # Okay: 20-30% + ("#F8A8A8", 30, 20), # Bad: 30-50% + ] + else: # 60-79 + segments = [ + ("#F8A8A8", 0, 5), # Bad: 0-5% + ("#FFEECC", 5, 5), # Okay: 5-10% + ("#D0F0C0", 10, 15), # Good: 10-25% + ("#FFEECC", 25, 5), # Okay: 25-30% + ("#F8A8A8", 30, 20), # Bad: 30-50% + ] fig, ax = plt.subplots(figsize=(10, 2)) @@ -779,10 +905,40 @@ class GraphGenerator: Returns: Base64 string or file path """ - # Coerce numeric columns - for col in ["Best", "LLN", "Pred.", "%Pred.", "ZScore"]: - if col in spirometry_df.columns: - spirometry_df[col] = pd.to_numeric(spirometry_df[col], errors="coerce") + # Coerce numeric columns - handle various column name formats + # Map standard column names to possible variations + column_aliases = { + "Best": ["Best", "best", "BEST"], + "LLN": ["LLN", "lln"], + "Pred.": ["Pred.", "Pred", "pred", "Predicted", "predicted"], + "%Pred.": [ + "%Pred.", + "%Pred", + "%pred", + "% Pred.", + "% Pred", + "Pred %", + "Pred%", + ], + "ZScore": ["ZScore", "Z-Score", "z-score", "Zscore", "zscore", "Z Score"], + } + + # Find and normalize column names + column_mapping = {} + for target_col, possible_names in column_aliases.items(): + for col_name in possible_names: + if col_name in spirometry_df.columns: + column_mapping[target_col] = col_name + # Convert to numeric + spirometry_df[col_name] = pd.to_numeric( + spirometry_df[col_name], errors="coerce" + ) + break + + # If standard columns don't exist, create aliases + for target_col, source_col in column_mapping.items(): + if target_col not in spirometry_df.columns and source_col != target_col: + spirometry_df[target_col] = spirometry_df[source_col] # Select rows of interest rows_map = { @@ -793,20 +949,49 @@ class GraphGenerator: records = [] for label, param in rows_map.items(): + # Try exact match first row = spirometry_df.loc[spirometry_df["Parameters"].str.strip() == param] if row.empty: + # Try case-insensitive match + row = spirometry_df.loc[ + spirometry_df["Parameters"].str.strip().str.upper() == param.upper() + ] + if row.empty: + # Try matching without % sign + if "%" in param: + param_no_pct = param.replace("%", "") + row = spirometry_df.loc[ + spirometry_df["Parameters"].str.strip() == param_no_pct + ] + if row.empty: + print(f"Warning: Could not find parameter '{param}' in spirometry data") + print(f"Available parameters: {spirometry_df['Parameters'].tolist()}") continue row = row.iloc[0] + # Get values with fallbacks for column name variations + best_val = row.get("Best", row.get("best", pd.NA)) + pct_val = row.get( + "%Pred.", row.get("%Pred", row.get("Pred %", row.get("Pred%", pd.NA))) + ) + z_val = row.get("ZScore", row.get("Z-Score", row.get("Zscore", pd.NA))) + records.append( { "label": label, "param": param, - "best": row["Best"], - "pct": row["%Pred."], - "z": row["ZScore"], + "best": best_val, + "pct": pct_val, + "z": z_val, } ) + # Validate we have exactly 3 records + if len(records) != 3: + raise ValueError( + f"Expected 3 spirometry parameters (FVC, FEV1, FEV1/FVC%), " + f"but found {len(records)}. Found: {[r['param'] for r in records]}" + ) + # Figure setup fig, axes = plt.subplots( nrows=3, @@ -936,3 +1121,187 @@ class GraphGenerator: plt.close() return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path) + + def generate_metabolism_chart( + self, rmr_kcal: float, save_as_base64: bool = True + ) -> str: + """ + Generate metabolism chart (Slow vs Fast Metabolism). + + Args: + rmr_kcal: Resting metabolic rate in kcal/day + save_as_base64: If True, return base64 string, else return file path + + Returns: + Base64 string or file path + """ + from matplotlib.patches import FancyBboxPatch + + fig, ax = plt.subplots(figsize=(10, 2.5)) + + # Chart data and positions + categories = ["Very Slow", "Slow", "Average", "Fast", "Very Fast"] + positions = [1500, 3000, 4500, 6000, 7500] + indicator_pos = rmr_kcal + highlight_end = rmr_kcal + + # Main Bar (Background) + main_bar = FancyBboxPatch( + (0, 0.4), + 9000, + 0.2, + boxstyle="round,pad=0,rounding_size=0.1", + ec="none", + fc="#E0E0E0", + ) + ax.add_patch(main_bar) + + # Highlighted Bar + highlight_bar = FancyBboxPatch( + (0, 0.4), + highlight_end, + 0.2, + boxstyle="round,pad=0,rounding_size=0.1", + ec="none", + fc="#B2FFC8", + ) + ax.add_patch(highlight_bar) + + # Text and Labels + ax.text( + highlight_end / 2, + 0.5, + f"{rmr_kcal:.0f}kCals", + ha="center", + va="center", + color="#006400", + fontsize=14, + weight="bold", + ) + + # Indicator Triangle + ax.plot(indicator_pos, 0.65, "v", markersize=15, color="#606060", clip_on=False) + + # Ticks and Labels + for pos, label in zip(positions, categories): + ax.text( + pos, 0.15, label, ha="center", va="center", fontsize=12, color="#333333" + ) + ax.plot([pos, pos], [0.35, 0.39], color="grey", lw=5) + + # Chart Styling + ax.set_title("Slow vs Fast Metabolism", fontsize=18, weight="bold", loc="left") + ax.set_xlim(0, 9000) + ax.set_ylim(0, 1) + ax.axis("off") + + plt.tight_layout() + + chart_path = self.charts_dir / "metabolism_chart.png" + plt.savefig(chart_path, bbox_inches="tight", dpi=300) + plt.close() + + return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path) + + def generate_fuel_source_chart( + self, fat_percentage: float, save_as_base64: bool = True + ) -> str: + """ + Generate fuel source chart (Fats vs Carbs). + + Args: + fat_percentage: Fat percentage at rest + save_as_base64: If True, return base64 string, else return file path + + Returns: + Base64 string or file path + """ + from matplotlib.patches import FancyBboxPatch + + fig, ax = plt.subplots(figsize=(10, 2.5)) + + carb_percentage = 100 - fat_percentage + optimal_point = 75 + + # Main Bars (Fats and Carbs) + # Fats bar (yellow) + fats_bar = FancyBboxPatch( + (0, 0.4), + fat_percentage, + 0.2, + boxstyle="round,pad=0,rounding_size=0.1", + ec="none", + fc="#FEEAAB", + ) + ax.add_patch(fats_bar) + + # Carbs bar (blue) - starts where the fats bar ends + carbs_bar = FancyBboxPatch( + (fat_percentage, 0.4), + carb_percentage, + 0.2, + boxstyle="round,pad=0,rounding_size=0.1", + ec="none", + fc="#A7F5FF", + ) + ax.add_patch(carbs_bar) + + # Text and Labels + ax.text( + fat_percentage / 2, + 0.5, + f"Fats\n{fat_percentage:.1f}%", + ha="center", + va="center", + color="#333333", + fontsize=12, + weight="bold", + ) + ax.text( + fat_percentage + carb_percentage / 2, + 0.5, + f"Carbs\n{carb_percentage:.1f}%", + ha="center", + va="center", + color="#333333", + fontsize=12, + weight="bold", + ) + + # Add 'Optimal' label + ax.text(optimal_point, 0.75, "Optimal", ha="center", va="center", fontsize=12) + + # Indicator Triangle + ax.plot( + fat_percentage, 0.65, "v", markersize=15, color="#606060", clip_on=False + ) + + # Ticks and Labels + positions = [0, 25, 50, 75, 100] + for pos in positions: + ax.text( + pos, + 0.15, + str(pos), + ha="center", + va="center", + fontsize=12, + color="#333333", + ) + ax.plot([pos, pos], [0.35, 0.39], color="grey", lw=5) + + # Add a special tick for the 'Optimal' point + ax.plot([optimal_point, optimal_point], [0.6, 0.7], color="black", lw=2) + + # Chart Styling + ax.set_title("Fuel Source", fontsize=18, weight="bold", loc="left") + ax.set_ylim(0, 1) + ax.axis("off") + + plt.tight_layout() + + chart_path = self.charts_dir / "fuel_source_chart.png" + plt.savefig(chart_path, bbox_inches="tight", dpi=300) + plt.close() + + return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path) diff --git a/app/services/report_generator.py b/app/services/report_generator.py index fc8d4a6..56f37c5 100644 --- a/app/services/report_generator.py +++ b/app/services/report_generator.py @@ -151,7 +151,7 @@ class ReportGeneratorService: } def generate_html( - self, patient_info: Dict[str, Any], context_list: List[Dict[str, Any]] + self, patient_info: Dict[str, Any], contexts: Dict[str, Dict[str, Any]] ) -> str: """ Generate HTML content for the report. @@ -159,7 +159,7 @@ class ReportGeneratorService: Args: patient_info: Dictionary containing patient information (patient_name, age, height, weight, focus) - context_list: List of context dictionaries for each page + contexts: Dictionary with keys 'page_1', 'page_2', etc., each containing context data Returns: Complete HTML document as string @@ -175,6 +175,9 @@ class ReportGeneratorService: "focus": patient_info.get("focus", "Endurance"), } + # Get total number of pages + num_pages = len(contexts) + # Footer context footer_context = [ { @@ -183,7 +186,7 @@ class ReportGeneratorService: "social": "@ishplabs", "page_number": i + 1, } - for i in range(len(context_list)) + for i in range(num_pages) ] # Render header @@ -195,11 +198,13 @@ class ReportGeneratorService: for context in footer_context ] - # Render pages - for i, context in enumerate(context_list): - template = self.env.get_template(f"page_{i + 1}.html").render(context) + # Render pages - iterate through pages in order + for i in range(1, num_pages + 1): + page_key = f"page_{i}" + context = contexts.get(page_key, {}) + template = self.env.get_template(f"page_{i}.html").render(context) - if (i + 1) > 2: + if i > 2: full_html = f"""

@@ -209,7 +214,7 @@ class ReportGeneratorService: {template}
- {footer_html_list[i]} + {footer_html_list[i - 1]}
""" @@ -284,10 +289,10 @@ class ReportGeneratorService: self, spirometry_pdf_path: str, pnoe_csv_path: str, - seca_excel_path: str, patient_info: Dict[str, Any], output_filename: str = None, metric_overrides: Optional[Dict[str, Any]] = None, + oxygenation_csv_path: Optional[str] = None, ) -> Dict[str, Any]: """ Generate complete medical report from uploaded files. @@ -325,69 +330,165 @@ class ReportGeneratorService: graphs_generated = self.generate_graphs(df) # Create graph dictionary with base64 encoded images + import base64 + graphs_dict = {} for graph in graphs_generated: # Read the graph file and convert to base64 graph_path = Path(graph["path"]) if graph_path.exists(): - import base64 - with open(graph_path, "rb") as f: graphs_dict[graph["name"]] = base64.b64encode(f.read()).decode( "utf-8" ) # Also generate body composition charts - # Extract patient data for these charts - patient_name = patient_info.get("patient_name", "").split()[-1] # Get last name + # Use patient info directly (no SECA file needed) + fat_pct = patient_info.get("fat_percentage", 0) + age = patient_info.get("age", 25) + gender = patient_info.get("gender", "female").lower() - # Load SECA data to get body composition info - seca_df = pd.read_excel(seca_excel_path) - patient_data = seca_df[ - seca_df["LastName"].str.contains(patient_name, case=False, na=False) - ] + # Convert weight to kg if needed + weight_str = str(patient_info.get("weight", "0")) + # Extract numeric value and unit + weight_str_clean = ( + weight_str.replace("lbs", "").replace("kg", "").replace(" ", "").strip() + ) + try: + weight_value = float(weight_str_clean) + except ValueError: + print(f"Warning: Could not parse weight '{weight_str}', using default 0") + weight_value = 0.0 - if not patient_data.empty: - row = patient_data.iloc[0] - weight_kg = float(row.get("Weight", 0)) - fat_pct = float(row.get("Adult_FMP", 0)) - age = int(row.get("Age", patient_info.get("age", 25))) - gender = row.get("Gender", "female").lower() + # Convert to kg if weight is in lbs + if "lbs" in weight_str.lower(): + weight_kg = weight_value / 2.20462 # Convert lbs to kg + else: + weight_kg = weight_value # Already in kg or assume kg if no unit specified - fat_mass_lbs = weight_kg * fat_pct / 100 * 2.20462 - lean_mass_lbs = weight_kg * (1 - fat_pct / 100) * 2.20462 + # Calculate fat and lean mass in pounds + fat_mass_lbs = weight_kg * fat_pct / 100 * 2.20462 + lean_mass_lbs = weight_kg * (1 - fat_pct / 100) * 2.20462 - # Generate body composition chart - body_comp_b64 = self.graph_generator.generate_body_composition_chart( - fat_mass_lbs, lean_mass_lbs, save_as_base64=True + # Generate body composition chart (save as file first, then convert to base64) + try: + body_comp_path = self.graph_generator.generate_body_composition_chart( + fat_mass_lbs, lean_mass_lbs, save_as_base64=False ) - graphs_dict["body_composition"] = body_comp_b64 - - # Generate body fat percent chart - body_fat_b64 = self.graph_generator.generate_body_fat_percent_chart( - fat_pct, age, gender, save_as_base64=True + graphs_generated.append( + {"name": "body_composition", "path": str(body_comp_path)} ) - graphs_dict["body_fat_percent"] = body_fat_b64 + # Convert to base64 for graphs_dict + with open(body_comp_path, "rb") as f: + graphs_dict["body_composition"] = base64.b64encode(f.read()).decode( + "utf-8" + ) + except Exception as e: + print(f"Warning: Could not generate body composition chart: {e}") + graphs_dict["body_composition"] = "" + + # Generate body fat percent chart (save as file first, then convert to base64) + try: + body_fat_path = self.graph_generator.generate_body_fat_percent_chart( + fat_pct, age, gender, save_as_base64=False + ) + graphs_generated.append( + {"name": "body_fat_percent", "path": str(body_fat_path)} + ) + # Convert to base64 for graphs_dict + with open(body_fat_path, "rb") as f: + graphs_dict["body_fat_percent"] = base64.b64encode(f.read()).decode( + "utf-8" + ) + except Exception as e: + print(f"Warning: Could not generate body fat percent chart: {e}") + graphs_dict["body_fat_percent"] = "" # Generate spirometry chart print("Step 4: Generating spirometry chart...") try: spirometry_df = pd.read_csv(spirometry_csv_path) print(f"Spirometry data loaded: {len(spirometry_df)} rows") + print(f"Spirometry columns: {spirometry_df.columns.tolist()}") + if "Parameters" in spirometry_df.columns: + print(f"Available parameters: {spirometry_df['Parameters'].tolist()}") spirometry_chart_b64 = self.graph_generator.generate_spirometry_chart( spirometry_df, save_as_base64=True ) graphs_dict["spirometry_chart"] = spirometry_chart_b64 + 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}") graphs_dict["spirometry_chart"] = "" + # Generate TSI chart if oxygenation CSV is provided + if oxygenation_csv_path: + print("Step 4.5: Generating TSI chart...") + try: + oxygenation_df = pd.read_csv(oxygenation_csv_path) + tsi_chart_b64 = self.graph_generator.generate_tsi_chart( + oxygenation_df, save_as_base64=True + ) + graphs_dict["tsi_chart"] = tsi_chart_b64 + except Exception as e: + print(f"Warning: Could not generate TSI chart: {e}") + graphs_dict["tsi_chart"] = "" + + # Generate metabolism and fuel source charts for page 5 + print("Step 4.6: Generating metabolism and fuel source charts...") + try: + # Calculate RMR and fuel source from pnoe data + from services.context_generator import ContextGenerator + + temp_context_gen = ContextGenerator() + temp_context_gen.load_data(pnoe_csv_path, str(spirometry_csv_path), None) + temp_context_gen.patient_info = { + "name": patient_info.get("first_name", ""), + "last_name": patient_info.get("last_name", ""), + "age": patient_info.get("age", 25), + "weight": weight_kg, + "fat_percentage": fat_pct, + "gender": gender, + } + rmr_metrics = temp_context_gen.calculate_rmr_and_fuel_source() + + # Generate metabolism chart + metabolism_chart_b64 = self.graph_generator.generate_metabolism_chart( + rmr_metrics["rmr_kcal"], save_as_base64=True + ) + graphs_dict["metabolism_chart"] = metabolism_chart_b64 + + # Generate fuel source chart + fuel_source_chart_b64 = self.graph_generator.generate_fuel_source_chart( + rmr_metrics["rest_fat_percentage"], save_as_base64=True + ) + graphs_dict["fuel_source_chart"] = fuel_source_chart_b64 + except Exception as e: + print(f"Warning: Could not generate metabolism/fuel source charts: {e}") + graphs_dict["metabolism_chart"] = "" + graphs_dict["fuel_source_chart"] = "" + # Step 5: Generate context for all pages print("Step 5: Generating page contexts...") + patient_name = patient_info.get("patient_name", "") self.context_generator.load_data( - pnoe_csv_path, str(spirometry_csv_path), seca_excel_path + pnoe_csv_path, + str(spirometry_csv_path), + None, # No SECA file ) - context_list = self.context_generator.generate_all_contexts( + # Set patient info manually + self.context_generator.patient_info = { + "name": patient_info.get("first_name", ""), + "last_name": patient_info.get("last_name", ""), + "age": patient_info.get("age", 25), + "weight": weight_kg, + "fat_percentage": fat_pct, + "gender": gender, + } + contexts = self.context_generator.generate_all_contexts( patient_name, graphs_dict, metric_overrides=metric_overrides ) @@ -396,7 +497,7 @@ class ReportGeneratorService: analysis_data["graphs_count"] = len(graphs_generated) # Step 6: Generate HTML - html_content = self.generate_html(patient_info, context_list) + html_content = self.generate_html(patient_info, contexts) # Step 7: Generate PDF if output_filename is None: diff --git a/app/templates/base.html b/app/templates/base.html index be3035b..e853860 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -39,3 +39,4 @@ + diff --git a/app/templates/upload.html b/app/templates/upload.html index eb07777..6f18a5a 100644 --- a/app/templates/upload.html +++ b/app/templates/upload.html @@ -84,9 +84,16 @@ class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
- - Body Fat Percentage (%) + +
+
+ + +

Upload NIRS muscle oxygen CSV file to generate TSI graph

diff --git a/notebooks/graphs.ipynb b/notebooks/graphs.ipynb index 3d3d4a2..6fdef69 100644 --- a/notebooks/graphs.ipynb +++ b/notebooks/graphs.ipynb @@ -72,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "id": "99116a35", "metadata": {}, "outputs": [], @@ -237,7 +237,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "id": "470e871e", "metadata": {}, "outputs": [ @@ -290,7 +290,14 @@ " # --- Chart data and positions ---\n", " categories = ['Very Slow', 'Slow', 'Average', 'Fast', 'Very Fast']\n", " positions = [1500, 3000, 4500, 6000, 7500]\n", - " kcal_value = 1386\n", + " # Step 1: Filter resting phase (usually lowest VO2 or MET values)\n", + " rest_phase = df[df['MET'] <= 1.1] # assuming <1.1 MET means rest\n", + "\n", + " # Step 2: Compute resting metabolic rate\n", + " rmr = rest_phase['EE(kcal/day)'].mean()\n", + "\n", + " print(f\"Estimated RMR from data: {rmr:.0f} kcal/day\")\n", + " kcal_value = rmr\n", " # Position the indicator and highlight based on the kcal value\n", " # For this example, we'll place it in the 'Very Slow' section.\n", " indicator_pos = kcal_value\n", @@ -349,7 +356,13 @@ " fig, ax = plt.subplots(figsize=(10, 2.5))\n", "\n", " # --- Chart data and positions ---\n", - " fat_percentage = 33\n", + " rest_phase = df[df['RER'] == 0.9] # filter rest data\n", + " fat_rest = rest_phase['FAT(%)'].mean()\n", + " carb_rest = rest_phase['CARBS(%)'].mean()\n", + "\n", + " print(f\"Resting phase fuel mix: Fats {fat_rest:.1f}%, Carbs {carb_rest:.1f}%\")\n", + "\n", + " fat_percentage = fat_rest\n", " carb_percentage = 100 - fat_percentage\n", " optimal_point = 75\n", "\n", diff --git a/notebooks/page_context_gen.ipynb b/notebooks/page_context_gen.ipynb index fdd6db7..d89e919 100644 --- a/notebooks/page_context_gen.ipynb +++ b/notebooks/page_context_gen.ipynb @@ -18,13 +18,12 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "da5ac3c1", "metadata": {}, "outputs": [], "source": [ "pnoe_df = pd.read_csv('data/pnoe_data.csv', delimiter=';')\n", - "patients_info = pd.read_excel('data/patients_data.xlsx')\n", "spirometry_df = pd.read_csv('data/spirometry_data.csv')" ] }, @@ -254,7 +253,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "2fa8ff13", "metadata": {}, "outputs": [ @@ -270,15 +269,10 @@ } ], "source": [ - "def body_composition_chart(first_name='Keirstyn', last_name='Moran'):\n", + "def body_composition_chart(fat_percentage=22.4, weight_kg=70):\n", "\n", " \n", " #=========================== Body Composition Donut Chart ========================#\n", - " patient_data = patients_info[(patients_info['FirstName'].str.contains(first_name, case=False, na=False)) & \n", - " (patients_info['LastName'].str.contains(last_name, case=False, na=False))]\n", - "# Get the fat mass percentage for Keirstyn\n", - " fat_percentage = patient_data['Adult_FMP'].iloc[0]\n", - " weight_kg = patient_data['Weight'].iloc[0]\n", " lean_percentage = 100 - fat_percentage\n", "\n", "# Create donut chart\n",