From 777b3de33824e8438fac105f69e3d56c1e961ade Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 14 May 2026 10:15:14 +0200 Subject: [PATCH] sostituisce pylatex con reportlab: genera PDF senza LaTeX di sistema Co-Authored-By: Claude Sonnet 4.6 --- report_completo.py | 259 ++++++++++++++++++++++++--------------------- requirements.txt | 2 +- 2 files changed, 140 insertions(+), 121 deletions(-) diff --git a/report_completo.py b/report_completo.py index 1ac3faf..28b7d6e 100644 --- a/report_completo.py +++ b/report_completo.py @@ -1,152 +1,171 @@ from pathlib import Path -from pylatex import (Document, Section, Subsection, Command, Figure, - Tabular, Math, NoEscape, Package) -from pylatex.utils import bold +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import cm +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib import colors +from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, + TableStyle, Image, HRFlowable, PageBreak) BASE = Path(__file__).parent INDUTTORI = [ { - "nome": "Induttore 1", + "nome": "Induttore 1", "intervallo": "110.81 – 111.56 s", - "t0": "110.81", - "t_end": "111.56", - "n_punti": "5", - "zona_exc": None, - "A": "87.3140", - "A_err": "0.0260", - "tau": "15.7231", - "tau_err": "0.2021", - "r2": "0.999505", - "png": BASE / "induttore1" / "induttore1.png", + "t0": "110.81", + "t_end": "111.56", + "n_punti": "5", + "zona_exc": None, + "A": "87.3140", + "A_err": "0.0260", + "tau": "15.7231", + "tau_err": "0.2021", + "r2": "0.999505", + "png": BASE / "induttore1" / "induttore1.png", }, { - "nome": "Induttore 2", + "nome": "Induttore 2", "intervallo": "112.02 – 113.95 s", - "t0": "112.02", - "t_end": "113.95", - "n_punti": "13", - "zona_exc": None, - "A": "158.3430", - "A_err": "0.2088", - "tau": "13.2844", - "tau_err": "0.2183", - "r2": "0.997314", - "png": BASE / "induttore2" / "induttore2.png", + "t0": "112.02", + "t_end": "113.95", + "n_punti": "13", + "zona_exc": None, + "A": "158.3430", + "A_err": "0.2088", + "tau": "13.2844", + "tau_err": "0.2183", + "r2": "0.997314", + "png": BASE / "induttore2" / "induttore2.png", }, { - "nome": "Induttore 3", + "nome": "Induttore 3", "intervallo": "115.0 – 133.7 s", - "t0": "115.0", - "t_end": "133.7", - "n_punti": "125", - "zona_exc": "115.9 – 117.2 s", - "A": "185.1847", - "A_err": "0.2706", - "tau": "16.2654", - "tau_err": "0.0464", - "r2": "0.993827", - "png": BASE / "induttore3" / "induttore3.png", + "t0": "115.0", + "t_end": "133.7", + "n_punti": "125", + "zona_exc": "115.9 – 117.2 s", + "A": "185.1847", + "A_err": "0.2706", + "tau": "16.2654", + "tau_err": "0.0464", + "r2": "0.993827", + "png": BASE / "induttore3" / "induttore3.png", }, ] +HEADER_COLOR = colors.HexColor("#2c5f8a") +LIGHT_GRAY = colors.HexColor("#f2f2f2") + +TABLE_STYLE = TableStyle([ + ("BACKGROUND", (0, 0), (-1, 0), HEADER_COLOR), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("FONTSIZE", (0, 0), (-1, 0), 10), + ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT_GRAY]), + ("FONTNAME", (0, 1), (-1, -1), "Helvetica"), + ("FONTSIZE", (0, 1), (-1, -1), 9), + ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#cccccc")), + ("TOPPADDING", (0, 0), (-1, -1), 5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 5), + ("LEFTPADDING", (0, 0), (-1, -1), 8), +]) + + def build_doc(): - doc = Document(geometry_options={"margin": "2.5cm"}) + out = str(BASE / "report_completo.pdf") + doc = SimpleDocTemplate(out, pagesize=A4, + leftMargin=2.5*cm, rightMargin=2.5*cm, + topMargin=2.5*cm, bottomMargin=2.5*cm) - doc.packages.append(Package("booktabs")) - doc.packages.append(Package("graphicx")) - doc.packages.append(Package("amsmath")) - doc.packages.append(Package("float")) - doc.packages.append(Package("caption")) - doc.packages.append(Package("inputenc", options="utf8")) + styles = getSampleStyleSheet() + style_title = styles["Title"] + style_h1 = styles["Heading1"] + style_h2 = styles["Heading2"] + style_body = styles["BodyText"] + style_eq = ParagraphStyle("Equation", parent=styles["BodyText"], + fontName="Courier", fontSize=10, + leftIndent=20, spaceAfter=6) - # Titolo - doc.preamble.append(Command("title", "Report Analisi Termica Forno")) - doc.preamble.append(Command("author", "Analisi IR -- Cevolani")) - doc.preamble.append(Command("date", NoEscape(r"\today"))) - doc.append(NoEscape(r"\maketitle")) + story = [] - # Introduzione - with doc.create(Section("Modello di fitting")): - doc.append( - "Ogni intervallo è interpolato con il modello di raffreddamento di Newton:" - ) - doc.append(Math(data=[NoEscape( - r"T(t) = T_\infty + A \cdot e^{-(t - t_0)/\tau}" - )], escape=False)) - doc.append(NoEscape( - r"dove $T_\infty = 22.99\,^\circ\mathrm{C}$ è la temperatura ambiente media ponderata. " - r"La regressione non lineare usa il metodo \textit{Trust Region Reflective} (TRF) " - r"di \texttt{scipy.optimize.curve\_fit}." - )) + # --- Titolo --- + story.append(Paragraph("Report Analisi Termica Forno", style_title)) + story.append(Paragraph("Analisi IR – Cevolani", styles["Normal"])) + story.append(Spacer(1, 0.5*cm)) + story.append(HRFlowable(width="100%", thickness=1, color=HEADER_COLOR)) + story.append(Spacer(1, 0.5*cm)) - # Sezione per ogni induttore + # --- Modello --- + story.append(Paragraph("Modello di fitting", style_h1)) + story.append(Paragraph( + "Ogni intervallo è interpolato con il modello di raffreddamento di Newton:", + style_body)) + story.append(Spacer(1, 0.2*cm)) + story.append(Paragraph("T(t) = T_inf + A · exp( -(t - t0) / τ )", style_eq)) + story.append(Paragraph( + "dove T_inf = 22.99 °C è la temperatura ambiente media ponderata. " + "La regressione non lineare usa il metodo Trust Region Reflective (TRF) " + "di scipy.optimize.curve_fit.", + style_body)) + story.append(Spacer(1, 0.4*cm)) + + # --- Sezione per ogni induttore --- for ind in INDUTTORI: - with doc.create(Section(ind["nome"])): + story.append(HRFlowable(width="100%", thickness=0.5, color=colors.lightgrey)) + story.append(Spacer(1, 0.2*cm)) + story.append(Paragraph(ind["nome"], style_h1)) - with doc.create(Subsection("Intervallo di campionamento")): - with doc.create(Tabular("|l|l|")) as tab: - tab.add_hline() - tab.add_row([bold("Parametro"), bold("Valore")]) - tab.add_hline() - tab.add_row(["Inizio", f"{ind['t0']} s"]) - tab.add_row(["Fine", f"{ind['t_end']} s"]) - tab.add_row(["Numero di punti", ind["n_punti"]]) - if ind["zona_exc"]: - tab.add_row(["Zona esclusa (peso = 0)", ind["zona_exc"]]) - tab.add_hline() + # Tabella intervallo + story.append(Paragraph("Intervallo di campionamento", style_h2)) + rows = [["Parametro", "Valore"], + ["Inizio", f"{ind['t0']} s"], + ["Fine", f"{ind['t_end']} s"], + ["Numero di punti", ind["n_punti"]]] + if ind["zona_exc"]: + rows.append(["Zona esclusa (peso = 0)", ind["zona_exc"]]) + t = Table(rows, colWidths=[8*cm, 6*cm]) + t.setStyle(TABLE_STYLE) + story.append(t) + story.append(Spacer(1, 0.3*cm)) - with doc.create(Subsection("Equazione trovata")): - t0 = ind["t0"] - A = ind["A"] - tau = ind["tau"] - doc.append(Math(data=[NoEscape( - rf"T(t) = 22.99 + {A} \cdot e^{{-(t - {t0}) / {tau}}}" - )], escape=False)) + # Equazione + story.append(Paragraph("Equazione trovata", style_h2)) + eq = (f"T(t) = 22.99 + {ind['A']} · exp( -(t - {ind['t0']}) / {ind['tau']} )") + story.append(Paragraph(eq, style_eq)) + story.append(Spacer(1, 0.2*cm)) - doc.append(NoEscape(r"\vspace{4pt}")) - with doc.create(Tabular("|l|l|l|")) as tab: - tab.add_hline() - tab.add_row([bold("Parametro"), bold("Valore"), bold("Incertezza (1σ)")]) - tab.add_hline() - tab.add_row(["A", - NoEscape(rf"${ind['A']}\,^\circ\mathrm{{C}}$"), - NoEscape(rf"$\pm\,{ind['A_err']}\,^\circ\mathrm{{C}}$")]) - tab.add_row([NoEscape(r"$\tau$"), - NoEscape(rf"${ind['tau']}\,\mathrm{{s}}$"), - NoEscape(rf"$\pm\,{ind['tau_err']}\,\mathrm{{s}}$")]) - tab.add_hline() + # Tabella parametri + param_rows = [ + ["Parametro", "Valore", "Incertezza (1σ)"], + ["A", f"{ind['A']} °C", f"± {ind['A_err']} °C"], + ["τ", f"{ind['tau']} s", f"± {ind['tau_err']} s"], + ["R²", ind["r2"], ""], + ] + t2 = Table(param_rows, colWidths=[4*cm, 6*cm, 5*cm]) + t2.setStyle(TABLE_STYLE) + story.append(t2) + story.append(Spacer(1, 0.3*cm)) - doc.append(NoEscape(r"\vspace{4pt}")) - doc.append(NoEscape(rf"$R^2 = {ind['r2']}$")) + # Plot + story.append(Paragraph("Grafico", style_h2)) + img = Image(str(ind["png"]), width=14*cm, height=8.4*cm) + story.append(img) + story.append(Spacer(1, 0.5*cm)) - with doc.create(Subsection("Grafico")): - with doc.create(Figure(position="H")) as fig: - fig.add_image(str(ind["png"]), width=NoEscape(r"0.9\textwidth")) - fig.add_caption(f"Fit esponenziale — {ind['nome']} [{ind['intervallo']}]") + # --- Tabella riepilogativa --- + story.append(PageBreak()) + story.append(Paragraph("Riepilogo", style_h1)) + summary = [["Induttore", "Intervallo [s]", "A [°C]", "τ [s]", "R²"]] + for ind in INDUTTORI: + summary.append([ind["nome"], ind["intervallo"], + ind["A"], ind["tau"], ind["r2"]]) + t3 = Table(summary, colWidths=[3.5*cm, 4.5*cm, 3*cm, 3*cm, 3*cm]) + t3.setStyle(TABLE_STYLE) + story.append(t3) - # Tabella riepilogativa - with doc.create(Section("Riepilogo")): - with doc.create(Tabular("|l|l|l|l|l|")) as tab: - tab.add_hline() - tab.add_row([bold("Induttore"), bold("Intervallo [s]"), - bold("A [°C]"), bold(NoEscape(r"$\tau$ [s]")), bold(NoEscape(r"$R^2$"))]) - tab.add_hline() - for ind in INDUTTORI: - tab.add_row([ind["nome"], ind["intervallo"], - ind["A"], ind["tau"], ind["r2"]]) - tab.add_hline() + doc.build(story) + print(f"report_completo.pdf generato.") - out = BASE / "report_completo" - doc.generate_tex(str(out)) - print(f"report_completo.tex generato.") - - try: - doc.generate_pdf(str(out), clean_tex=False, compiler="pdflatex") - print("report_completo.pdf generato.") - except Exception as e: - print(f"Compilazione PDF non disponibile ({e}).") - print("Installa MikTeX o TeX Live per generare il PDF da report_completo.tex.") if __name__ == "__main__": build_doc() diff --git a/requirements.txt b/requirements.txt index 4a791a3..7685747 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ pandas matplotlib scipy -pylatex +reportlab