From 28293ef132d5bd8baba01fa621b177ad89f6f50a Mon Sep 17 00:00:00 2001 From: Savio Cardoso Date: Sat, 15 Aug 2026 13:17:55 +0200 Subject: [PATCH] feat: add automated engineering reporting --- pyproject.toml | 5 +- reports/BodySimPy_Engineering_Summary.pdf | Bin 0 -> 2744 bytes reports/BodySimPy_Management_Summary.pptx | Bin 0 -> 34097 bytes scripts/generate_reports.py | 25 ++++ src/bodysimpy/reporting/pdf_report.py | 124 ++++++++++++++++++++ src/bodysimpy/reporting/pptx_report.py | 133 ++++++++++++++++++++++ src/bodysimpy/reporting/summary_data.py | 58 ++++++++++ 7 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 reports/BodySimPy_Engineering_Summary.pdf create mode 100644 reports/BodySimPy_Management_Summary.pptx create mode 100644 scripts/generate_reports.py create mode 100644 src/bodysimpy/reporting/pdf_report.py create mode 100644 src/bodysimpy/reporting/pptx_report.py create mode 100644 src/bodysimpy/reporting/summary_data.py diff --git a/pyproject.toml b/pyproject.toml index fed3283..d6e42e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,9 @@ dependencies = [ "PyYAML>=6.0", "pydantic>=2.7", "typer>=0.12", - "rich>=13.7" + "reportlab>=4.2", + "python-pptx>=1.0", + "rich>=13.7", ] [project.optional-dependencies] @@ -33,6 +35,7 @@ dev = [ "types-PyYAML", "scipy-stubs", "pandas-stubs", + "types-reportlab", ] ml = [ diff --git a/reports/BodySimPy_Engineering_Summary.pdf b/reports/BodySimPy_Engineering_Summary.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3b044a82f4ae581e6e56a9e6bf963cad52b1c167 GIT binary patch literal 2744 zcma);*_xtQ636d-3fvGEWD`VFKtXX|QPW-7L{bC=?5UcId4cJ>zMp5ELsn&VepT~L zHy6SQ;`}4d@5GTvPnt4H6ic!6AHV9fY@mL5H>UoTNS-8&=!nwh_28@vP?4&TOx`qTVbI+giTaG z;#&STV`hG18$j<&BTNxwpf&M5T`@x-g+&{J&CM?WVfUt5ruh9XeZP01Jpck>GcN=i z2&Q9mUv=LlBYO<`KsN!CKcS<6>j%2%)zC8l-$h`KJQqAfrr;ydZ|o&CR2|Ryq``dB zXgtG<=R?R2QC-jddh?#aE}Mlg$v4%|_S|Na9n!3lILY8FK@${FCQEso%;0$D!|$8G z(+%#XZl7(_c$es#U;p9xMgSqe59Ik*Qh=WE)J>4gpA{Zq9N3z#`d=Xw+I_0*C`3dF z6x^GsgQDLdKPND^LkA@P^U?t5`yHGaOje!XGuvo?KiEp%^L<~jz71Beg`yIgMYZq^ z>O1F8NTXuk6B2#O*ir62^zZ?U!- zaye_bN#k&J=;XCEVu^!?62Om&a|~PA8rhL<4@WBB84I>d)nr~4c*;4T7K}{`(?=&O z+?VI}oTbQKbH94zvlG~e;&e59(eGIRQjp@wOOIvJK{a)o%z!CaU z%~!en3Zn=0x~gKYKXNnss<{ zBT_<9au7{o)7)!`+wnPaKCNC#RAVldd(1LfUln?pmo3#w$Jf33a6S!C0~UD6;q}Q~ zkffV%VGz9gcI0yBKy8p4g=ajZ#&L)p^PUVHzavxohutW zX!)2eiv4j5tM5`s`g+NUE2-;dSIKoVWz?I?Ig#h;oki}rvbK&~WUmZerd{f)obMO8 z7EEh>Lm3q|T$tSU^V*2dYh$70xhkyc+oWoz&g%wt;ZwQUwxeM)+aK=_jGbr=Q@OY9 zEg2_e$GsqQZJ2iqOyk;Cd&6Rj)ga$(RWtEJXDH+@Q!ORBOn(n!Cc9`UC=Zr1Co3vH z*dO17FTo7hcxebb*U#39qWM?rN&SlsJoCaEaM^^&j(nha+bXytm*-2@`^!&1sSW6pg zF-X+TyIyi<&2*g_+%aXnDtG)&I-WgE^9dojy-aQu1x9$Hles%-7=w`l-(q*Hu=ZG_ zKwF8Yl5bUqL*x~Nd@VM+SY}>6U3)`Rn;$HmG0&RcVcfd<%pTT!+4W}|_0IXXPW42* z_6NO1uKIkNHErkA?}-;gk@*Kom(6WROWIXd;ZoIg4YaN@x89Bt+cX!zS-at$#m(BF zP0X$K7fETq9=(v#blfQ+DG!NvpVJjn*`B!?3)iHnJ5Y`*h49f0e#JU)BJ3isIHsc9 z9EV|e%{TRpo|E1BCex;ZUZs25@E!*9H2!#-aQg6U$4%HzjxqOvQ=>&Eowv4y{e)gE zXK1tboH$8xJb5_`pZsDa;N;Ff;%)drHD{;6jnXr|coA<@Gt~(?qGaH$R@G?*mcGaT z#e)Xs`=96cR_2Ruo?|#viQzvkh@okUhR)Cr8v{Pt(FM6bYy=LPpr33cUirmFk-yHR zN$`pIX)ay)RgW$IYO7G6^g>^?9n*hr*t$(9wykNTd8SBBKbvI=$2F~@vn7(&aa^bM zl4cP6UwiCh=}^}qW@6om{M^bbFEkTI<5o;@h)8fG!juy`R9bs^wcvHxiT%I_a9 S)eqmXlN3$G(&^S%j{O(F)D5Bl literal 0 HcmV?d00001 diff --git a/reports/BodySimPy_Management_Summary.pptx b/reports/BodySimPy_Management_Summary.pptx new file mode 100644 index 0000000000000000000000000000000000000000..263830266a5510b221bd80f2be35028ee1494991 GIT binary patch literal 34097 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 O@COSBYoa>;diP&N->5DC literal 0 HcmV?d00001 diff --git a/scripts/generate_reports.py b/scripts/generate_reports.py new file mode 100644 index 0000000..82bfd38 --- /dev/null +++ b/scripts/generate_reports.py @@ -0,0 +1,25 @@ +from bodysimpy.reporting.pdf_report import generate_engineering_summary_pdf +from bodysimpy.reporting.pptx_report import generate_management_summary_pptx +from bodysimpy.reporting.summary_data import load_reporting_summary + + +def main() -> None: + summary = load_reporting_summary() + + generate_engineering_summary_pdf( + summary, + "reports/BodySimPy_Engineering_Summary.pdf", + ) + + generate_management_summary_pptx( + summary, + "reports/BodySimPy_Management_Summary.pptx", + ) + + print("Generated reports:") + print("reports/BodySimPy_Engineering_Summary.pdf") + print("reports/BodySimPy_Management_Summary.pptx") + + +if __name__ == "__main__": + main() diff --git a/src/bodysimpy/reporting/pdf_report.py b/src/bodysimpy/reporting/pdf_report.py new file mode 100644 index 0000000..adfc584 --- /dev/null +++ b/src/bodysimpy/reporting/pdf_report.py @@ -0,0 +1,124 @@ +from pathlib import Path + +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus import ( + Flowable, + Paragraph, + SimpleDocTemplate, + Spacer, +) + +from bodysimpy.reporting.summary_data import ReportingSummary + + +def _value( + value: float | None, + suffix: str = "", +) -> str: + if value is None: + return "Not available" + + return f"{value:.3f}{suffix}" + + +def generate_engineering_summary_pdf( + summary: ReportingSummary, + output_path: str | Path, +) -> None: + path = Path(output_path) + + path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + styles = getSampleStyleSheet() + + story: list[Flowable] = [] + + doc = SimpleDocTemplate( + str(path), + pagesize=A4, + ) + + story.append( + Paragraph( + "BodySimPy Engineering Summary", + styles["Title"], + ) + ) + + story.append( + Spacer( + 1, + 16, + ) + ) + + sections = { + "Objective": ( + "Automate structural CAE workflows for a simplified automotive " + "body crossmember surrogate using Python, analytical references, " + "CalculiX FEA, stochastic studies, fatigue assessment and ML surrogates." + ), + "Method": ( + "Validated configuration files generate structural models. " + "Analytical beam theory, CalculiX static/modal analysis and " + "Python post-processing are used to quantify response behavior." + ), + "Baseline Configuration": ( + "Thin-walled rectangular hollow-section cantilever surrogate with " + "linear-elastic isotropic material and idealized transverse loading." + ), + "Key Results": ( + f"Final mesh displacement error: " + f"{_value(summary.baseline_deflection_error_percent, ' %')}
" + f"Final mesh stress error: " + f"{_value(summary.baseline_stress_error_percent, ' %')}
" + f"Mode-1 frequency: " + f"{_value(summary.mode_1_frequency_hz, ' Hz')}
" + f"Observed stochastic stress exceedance over 350 MPa: " + f"{_value(summary.stress_exceedance_percent, ' %')}
" + f"Best ML frequency MAPE: " + f"{_value(summary.best_ml_frequency_mape_percent, ' %')}" + ), + "Sensitivity": ( + "Wall thickness, section geometry, Young's modulus, density and " + "load are evaluated through automated sweeps and stochastic analysis." + ), + "Risk": ( + "The model is a simplified surrogate, not a production body-in-white. " + "Results depend on idealized boundary conditions, beam assumptions, " + "generic material data and the sampled design domain." + ), + "Engineering Recommendation": ( + "Use BodySimPy as a reproducible CAE automation and validation " + "portfolio workflow. Extend shell modelling, fatigue realism and " + "solver-result extraction before claiming production-level accuracy." + ), + } + + for heading, text in sections.items(): + story.append( + Paragraph( + heading, + styles["Heading2"], + ) + ) + + story.append( + Paragraph( + text, + styles["BodyText"], + ) + ) + + story.append( + Spacer( + 1, + 12, + ) + ) + + doc.build(story) diff --git a/src/bodysimpy/reporting/pptx_report.py b/src/bodysimpy/reporting/pptx_report.py new file mode 100644 index 0000000..9744257 --- /dev/null +++ b/src/bodysimpy/reporting/pptx_report.py @@ -0,0 +1,133 @@ +from pathlib import Path + +from pptx import Presentation as create_presentation +from pptx.presentation import Presentation + +from bodysimpy.reporting.summary_data import ReportingSummary + + +def _value( + value: float | None, + suffix: str = "", +) -> str: + if value is None: + return "Not available" + + return f"{value:.3f}{suffix}" + + +def _add_title_slide( + presentation: Presentation, +) -> None: + slide = presentation.slides.add_slide(presentation.slide_layouts[0]) + + slide.shapes.title.text = "BodySimPy Management Summary" + + slide.placeholders[1].text = "Python-driven CAE workflow automation" + + +def _add_bullets( + presentation: Presentation, + title: str, + bullets: list[str], +) -> None: + slide = presentation.slides.add_slide(presentation.slide_layouts[1]) + + slide.shapes.title.text = title + + body = slide.placeholders[1].text_frame + body.clear() + + for index, bullet in enumerate(bullets): + if index == 0: + paragraph = body.paragraphs[0] + else: + paragraph = body.add_paragraph() + + paragraph.text = bullet + paragraph.level = 0 + + +def generate_management_summary_pptx( + summary: ReportingSummary, + output_path: str | Path, +) -> None: + path = Path(output_path) + + path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + presentation = create_presentation() + + _add_title_slide(presentation) + + _add_bullets( + presentation, + "Objective", + [ + ("Automate structural CAE workflows using Python."), + ("Validate FEA against analytical references."), + ("Generate reusable engineering evidence for portfolio discussion."), + ], + ) + + _add_bullets( + presentation, + "Method", + [ + "Validated YAML configuration.", + "Analytical cantilever reference solution.", + "CalculiX static and modal simulations.", + ("Stochastic, fatigue and ML surrogate studies."), + ], + ) + + _add_bullets( + presentation, + "Key Results", + [ + (f"Mesh displacement error: {_value(summary.baseline_deflection_error_percent, ' %')}"), + (f"Mesh stress error: {_value(summary.baseline_stress_error_percent, ' %')}"), + (f"Mode-1 frequency: {_value(summary.mode_1_frequency_hz, ' Hz')}"), + (f"Stress exceedance over 350 MPa: {_value(summary.stress_exceedance_percent, ' %')}"), + (f"Best ML frequency MAPE: {_value(summary.best_ml_frequency_mape_percent, ' %')}"), + ], + ) + + _add_bullets( + presentation, + "Sensitivity", + [ + ("Wall-thickness influence on stress, deflection, mass and modal response."), + ("Material stiffness and density influence on structural dynamics."), + ("Stochastic uncertainty propagation for manufacturing, material and load inputs."), + ], + ) + + _add_bullets( + presentation, + "Risk & Limitations", + [ + ("Simplified structural surrogate, not production body-in-white geometry."), + ("Idealized boundary conditions and generic material assumptions."), + ("ML surrogate is valid only inside the sampled engineering design space."), + ], + ) + + _add_bullets( + presentation, + "Engineering Recommendation", + [ + ("Use BodySimPy as a reproducible CAE automation and validation workflow."), + ("Use automated parameter studies to identify influential design variables."), + ( + "Extend shell modelling, joint modelling " + "and durability fidelity before " + "production-style conclusions." + ), + ], + ) + + presentation.save(str(path)) diff --git a/src/bodysimpy/reporting/summary_data.py b/src/bodysimpy/reporting/summary_data.py new file mode 100644 index 0000000..ca56352 --- /dev/null +++ b/src/bodysimpy/reporting/summary_data.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +from pathlib import Path + +import pandas as pd + + +@dataclass(frozen=True, slots=True) +class ReportingSummary: + project_name: str + baseline_deflection_error_percent: float | None + baseline_stress_error_percent: float | None + mode_1_frequency_hz: float | None + stress_exceedance_percent: float | None + best_ml_frequency_mape_percent: float | None + + +def _read_csv_if_exists(path: Path) -> pd.DataFrame | None: + if not path.exists(): + return None + return pd.read_csv(path) + + +def load_reporting_summary() -> ReportingSummary: + validation_dir = Path("docs/validation") + + static_mesh = _read_csv_if_exists(validation_dir / "static_mesh_convergence.csv") + modal = _read_csv_if_exists(validation_dir / "modal_frequencies.csv") + stochastic = _read_csv_if_exists(validation_dir / "stochastic_samples.csv") + ml = _read_csv_if_exists(validation_dir / "ml_sample_efficiency.csv") + + baseline_deflection_error = None + baseline_stress_error = None + + if static_mesh is not None and not static_mesh.empty: + final_row = static_mesh.iloc[-1] + baseline_deflection_error = float(final_row["deflection_error_percent"]) + baseline_stress_error = float(final_row["stress_error_percent"]) + + mode_1_frequency = None + if modal is not None and not modal.empty: + mode_1_frequency = float(modal.iloc[0]["frequency_hz"]) + + stress_exceedance = None + if stochastic is not None and not stochastic.empty: + stress_exceedance = float((stochastic["max_stress_mpa"] > 350.0).mean() * 100.0) + + best_ml_frequency_mape = None + if ml is not None and not ml.empty: + best_ml_frequency_mape = float(ml["frequency_mape_percent"].min()) + + return ReportingSummary( + project_name="BodySimPy", + baseline_deflection_error_percent=baseline_deflection_error, + baseline_stress_error_percent=baseline_stress_error, + mode_1_frequency_hz=mode_1_frequency, + stress_exceedance_percent=stress_exceedance, + best_ml_frequency_mape_percent=best_ml_frequency_mape, + )