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 0000000..3b044a8
Binary files /dev/null and b/reports/BodySimPy_Engineering_Summary.pdf differ
diff --git a/reports/BodySimPy_Management_Summary.pptx b/reports/BodySimPy_Management_Summary.pptx
new file mode 100644
index 0000000..2638302
Binary files /dev/null and b/reports/BodySimPy_Management_Summary.pptx differ
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,
+ )