diff --git a/report_completo.py b/report_completo.py index e8e7860..f663300 100644 --- a/report_completo.py +++ b/report_completo.py @@ -4,53 +4,68 @@ 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.lib.enums import TA_CENTER, TA_RIGHT +from reportlab.lib.enums import TA_CENTER, TA_LEFT from reportlab.platypus import (BaseDocTemplate, PageTemplate, Frame, Paragraph, Spacer, Table, TableStyle, - Image, HRFlowable, PageBreak, NextPageTemplate) + Image, HRFlowable, PageBreak, NextPageTemplate, + KeepTogether) from reportlab.pdfgen import canvas as rl_canvas from datetime import date BASE = Path(__file__).parent -# ─── Palette colori ─────────────────────────────────────────────────────────── -C_BLUE = colors.HexColor("#1a3a5c") -C_BLUE_LIGHT= colors.HexColor("#2c5f8a") -C_ACCENT = colors.HexColor("#e8f0f7") -C_GRAY = colors.HexColor("#f5f5f5") -C_BORDER = colors.HexColor("#c0c8d0") +AUTORE = "Ufficio Tecnico – Cevolani S.p.A." +VERSIONE = "1.0" +STATO = "Definitivo" -TABLE_HDR = TableStyle([ - ("BACKGROUND", (0, 0), (-1, 0), C_BLUE_LIGHT), - ("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, C_GRAY]), - ("FONTNAME", (0, 1), (-1, -1), "Helvetica"), - ("FONTSIZE", (0, 1), (-1, -1), 9), - ("GRID", (0, 0), (-1, -1), 0.4, C_BORDER), - ("TOPPADDING", (0, 0), (-1, -1), 5), - ("BOTTOMPADDING", (0, 0), (-1, -1), 5), - ("LEFTPADDING", (0, 0), (-1, -1), 8), - ("RIGHTPADDING", (0, 0), (-1, -1), 8), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), -]) +# ─── Palette ────────────────────────────────────────────────────────────────── +C_DARK = colors.HexColor("#0f2236") +C_BLUE = colors.HexColor("#1a3a5c") +C_MID = colors.HexColor("#2c5f8a") +C_LIGHT = colors.HexColor("#4a8fc0") +C_ACCENT = colors.HexColor("#e8f2fa") +C_GRAY = colors.HexColor("#f6f7f8") +C_BORDER = colors.HexColor("#b8c8d8") +C_TEXT = colors.HexColor("#1e1e1e") +C_WHITE = colors.white -TABLE_SUMMARY = TableStyle([ - ("BACKGROUND", (0, 0), (-1, 0), C_BLUE), - ("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, C_ACCENT]), - ("FONTNAME", (0, 1), (-1, -1), "Helvetica"), - ("FONTSIZE", (0, 1), (-1, -1), 9), +W, H = A4 +MARGIN = 2.5 * cm +CONTENT_W = W - 2 * MARGIN + +# ─── TableStyle ─────────────────────────────────────────────────────────────── +def make_table_style(header_color=None): + hc = header_color or C_MID + return TableStyle([ + ("BACKGROUND", (0, 0), (-1, 0), hc), + ("TEXTCOLOR", (0, 0), (-1, 0), C_WHITE), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("FONTSIZE", (0, 0), (-1, 0), 9), + ("ROWBACKGROUNDS", (0, 1), (-1, -1), [C_WHITE, C_GRAY]), + ("FONTNAME", (0, 1), (-1, -1), "Helvetica"), + ("FONTSIZE", (0, 1), (-1, -1), 9), + ("GRID", (0, 0), (-1, -1), 0.4, C_BORDER), + ("TOPPADDING", (0, 0), (-1, -1), 5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 5), + ("LEFTPADDING", (0, 0), (-1, -1), 8), + ("RIGHTPADDING", (0, 0), (-1, -1), 8), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ]) + +TABLE_STD = make_table_style() +TABLE_DARK = make_table_style(C_DARK) + +COVER_INFO_STYLE = TableStyle([ + ("FONTNAME", (0, 0), (-1, -1), "Helvetica"), + ("FONTSIZE", (0, 0), (-1, -1), 10), + ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"), + ("TEXTCOLOR", (0, 0), (0, -1), C_BLUE), + ("TEXTCOLOR", (1, 0), (1, -1), C_TEXT), + ("ROWBACKGROUNDS",(0, 0), (-1, -1), [C_WHITE, C_GRAY]), ("GRID", (0, 0), (-1, -1), 0.4, C_BORDER), - ("TOPPADDING", (0, 0), (-1, -1), 6), - ("BOTTOMPADDING", (0, 0), (-1, -1), 6), - ("LEFTPADDING", (0, 0), (-1, -1), 10), - ("RIGHTPADDING", (0, 0), (-1, -1), 10), - ("ALIGN", (2, 0), (-1, -1), "CENTER"), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("TOPPADDING", (0, 0), (-1, -1), 7), + ("BOTTOMPADDING", (0, 0), (-1, -1), 7), + ("LEFTPADDING", (0, 0), (-1, -1), 12), ]) @@ -74,8 +89,8 @@ def parse_md(md_path: Path) -> dict: m = re.search(r"\$\$(.+?)\$\$", text, re.DOTALL) data["equazione_latex"] = m.group(1).strip() if m else "" - eq = data["equazione_latex"].replace(r"\cdot", "·").replace(r"\infty", "∞") - data["equazione"] = eq + data["equazione"] = (data["equazione_latex"] + .replace(r"\cdot", "·").replace(r"\infty", "∞")) rows3 = re.findall(r"^\| (.+?) \| (.+?) \| (.+?) \|$", text, re.MULTILINE) params = {r[0].strip(): (r[1].strip(), r[2].strip()) @@ -98,10 +113,94 @@ def collect_induttori() -> list: for md in sorted(folder.glob("report*.md"))] +# ─── Canvas con header/footer ───────────────────────────────────────────────── +class ProfessionalCanvas(rl_canvas.Canvas): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._saved = [] + + def showPage(self): + self._saved.append(dict(self.__dict__)) + self._startPage() + + def save(self): + n = len(self._saved) + for state in self._saved: + self.__dict__.update(state) + if self._pageNumber > 1: + self._draw_header() + self._draw_footer(n) + super().showPage() + super().save() + + def _draw_header(self): + self.saveState() + # Barra colorata in cima + self.setFillColor(C_DARK) + self.rect(0, H - 1.4*cm, W, 1.4*cm, fill=1, stroke=0) + self.setFillColor(C_LIGHT) + self.rect(0, H - 1.4*cm, 0.5*cm, 1.4*cm, fill=1, stroke=0) + self.setFillColor(C_WHITE) + self.setFont("Helvetica-Bold", 9) + self.drawString(0.8*cm, H - 0.9*cm, "ANALISI TERMICA FORNO") + self.setFont("Helvetica", 8) + self.drawRightString(W - 0.6*cm, H - 0.9*cm, "Cevolani S.p.A. — Uso interno") + self.restoreState() + + def _draw_footer(self, total): + self.saveState() + self.setStrokeColor(C_MID) + self.setLineWidth(0.8) + self.line(MARGIN, 1.9*cm, W - MARGIN, 1.9*cm) + self.setFillColor(C_BLUE) + self.setFont("Helvetica", 7.5) + self.drawString(MARGIN, 1.4*cm, + f"Documento: report_completo | Ver. {VERSIONE} | {STATO}") + self.setFont("Helvetica-Bold", 8) + self.drawRightString(W - MARGIN, 1.4*cm, + f"Pag. {self._pageNumber} / {total}") + self.restoreState() + + +# ─── Stili testo ────────────────────────────────────────────────────────────── +def make_styles(): + styles = getSampleStyleSheet() + s = {} + s["body"] = ParagraphStyle("body", parent=styles["BodyText"], + fontSize=9.5, leading=14, textColor=C_TEXT, + spaceAfter=6) + s["h1"] = ParagraphStyle("h1", fontName="Helvetica-Bold", fontSize=14, + textColor=C_WHITE, spaceBefore=16, spaceAfter=2, + leftIndent=-0.3*cm, leading=18) + s["h2"] = ParagraphStyle("h2", fontName="Helvetica-Bold", fontSize=10.5, + textColor=C_BLUE, spaceBefore=10, spaceAfter=4, + leading=14) + s["eq"] = ParagraphStyle("eq", fontName="Courier", fontSize=9.5, + leftIndent=18, spaceBefore=4, spaceAfter=6, + backColor=C_ACCENT, borderPad=7, leading=14) + s["caption"] = ParagraphStyle("caption", fontName="Helvetica-Oblique", + fontSize=8.5, textColor=colors.HexColor("#555555"), + alignment=TA_CENTER, spaceAfter=4) + return s + + +def section_header(title: str, s_h1) -> list: + """Barra blu con titolo sezione.""" + tbl = Table([[Paragraph(title, s_h1)]], colWidths=[CONTENT_W]) + tbl.setStyle(TableStyle([ + ("BACKGROUND", (0, 0), (-1, -1), C_BLUE), + ("LEFTPADDING", (0, 0), (-1, -1), 10), + ("RIGHTPADDING", (0, 0), (-1, -1), 10), + ("TOPPADDING", (0, 0), (-1, -1), 6), + ("BOTTOMPADDING",(0, 0), (-1, -1), 6), + ])) + return [tbl, Spacer(1, 0.25*cm)] + + # ─── Genera .tex ────────────────────────────────────────────────────────────── def generate_tex(induttori: list): - lines = [] - lines += [ + today = date.today().strftime("%d/%m/%Y") + lines = [ r"\documentclass[11pt,a4paper]{article}", r"\usepackage[utf8]{inputenc}", r"\usepackage[T1]{fontenc}", @@ -110,18 +209,17 @@ def generate_tex(induttori: list): r"\usepackage{booktabs}", r"\usepackage{graphicx}", r"\usepackage{float}", - r"\usepackage{caption}", r"\usepackage[table]{xcolor}", r"\usepackage{hyperref}", - r"\definecolor{hdrblue}{HTML}{1a3a5c}", r"\usepackage{fancyhdr}", - r"\pagestyle{fancy}", - r"\fancyhf{}", - r"\rhead{\textcolor{hdrblue}{\small Analisi Termica Forno}}", - r"\lhead{\textcolor{hdrblue}{\small Cevolani}}", + r"\definecolor{darkblue}{HTML}{1a3a5c}", + r"\pagestyle{fancy}\fancyhf{}", + r"\rhead{\textcolor{darkblue}{\small Cevolani S.p.A.}}", + r"\lhead{\textcolor{darkblue}{\small Analisi Termica Forno}}", r"\rfoot{\thepage}", - r"\title{\textbf{Report Analisi Termica Forno}}", - r"\author{Analisi IR -- Cevolani}", + r"\lfoot{\textcolor{gray}{\tiny Ver. " + VERSIONE + r" -- " + today + r"}}", + rf"\title{{\textbf{{Report Analisi Termica Forno}}\\[0.5em]\large Caratterizzazione termica degli induttori}}", + rf"\author{{{AUTORE}}}", r"\date{\today}", r"\begin{document}", r"\maketitle", @@ -133,7 +231,7 @@ def generate_tex(induttori: list): r"\begin{equation}", r" T(t) = T_\infty + A \cdot e^{-(t-t_0)/\tau}", r"\end{equation}", - r"dove $T_\infty = 22.99\,^\circ\mathrm{C}$ è la temperatura ambiente media ponderata.", + 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}.", "", @@ -143,222 +241,194 @@ def generate_tex(induttori: list): lines += [ rf"\section{{{ind['nome']}}}", r"\subsection{Intervallo di campionamento}", - r"\begin{tabular}{ll}", - r"\toprule", - r"\textbf{Parametro} & \textbf{Valore} \\", - r"\midrule", + r"\begin{tabular}{ll}\toprule", + r"\textbf{Parametro} & \textbf{Valore} \\\midrule", rf"Inizio & ${ind['t0']}$ s \\", rf"Fine & ${ind['t_end']}$ s \\", rf"Numero di punti & {ind['n_punti']} \\", ] if ind["zona_exc"]: lines.append(rf"Zona esclusa (peso = 0) & {ind['zona_exc']} \\") - lines += [ - r"\bottomrule", - r"\end{tabular}", - "", + lines += [r"\bottomrule\end{tabular}", "", r"\subsection{Equazione trovata}", r"\begin{equation}", f" {ind['equazione_latex']}", r"\end{equation}", - r"\begin{tabular}{lll}", - r"\toprule", - r"\textbf{Parametro} & \textbf{Valore} & \textbf{Incertezza (1$\sigma$)} \\", - r"\midrule", + r"\begin{tabular}{lll}\toprule", + r"\textbf{Param.} & \textbf{Valore} & \textbf{Incertezza ($1\sigma$)} \\\midrule", rf"$A$ & ${ind['A']}\,^\circ\mathrm{{C}}$ & $\pm\,{ind['A_err']}\,^\circ\mathrm{{C}}$ \\", rf"$\tau$ & ${ind['tau']}\,\mathrm{{s}}$ & $\pm\,{ind['tau_err']}\,\mathrm{{s}}$ \\", rf"$R^2$ & \multicolumn{{2}}{{l}}{{{ind['r2']}}} \\", - r"\bottomrule", - r"\end{tabular}", - "", + r"\bottomrule\end{tabular}", "", r"\subsection{Grafico}", - r"\begin{figure}[H]", - r"\centering", + r"\begin{figure}[H]\centering", rf"\includegraphics[width=0.9\textwidth]{{{ind['png']}}}", rf"\caption{{Fit esponenziale -- {ind['nome']} [{ind['intervallo']}]}}", - r"\end{figure}", - r"\newpage", - "", + r"\end{figure}\newpage", "", ] - # Riepilogo lines += [ r"\section{Riepilogo}", - r"\begin{tabular}{lllll}", - r"\toprule", - r"\textbf{Induttore} & \textbf{Intervallo [s]} & \textbf{A [°C]} & \textbf{$\tau$ [s]} & \textbf{$R^2$} \\", - r"\midrule", + r"\begin{tabular}{lllll}\toprule", + r"\textbf{Induttore} & \textbf{Intervallo [s]} & \textbf{A [°C]} & \textbf{$\tau$ [s]} & \textbf{$R^2$} \\\midrule", ] for ind in induttori: lines.append( rf"{ind['nome']} & {ind['intervallo']} & {ind['A']} & {ind['tau']} & {ind['r2']} \\" ) - lines += [r"\bottomrule", r"\end{tabular}", "", r"\end{document}"] + lines += [r"\bottomrule\end{tabular}", "", r"\end{document}"] - out = BASE / "report_completo.tex" - out.write_text("\n".join(lines), encoding="utf-8") + (BASE / "report_completo.tex").write_text("\n".join(lines), encoding="utf-8") print("report_completo.tex generato.") -# ─── Header / footer per reportlab ─────────────────────────────────────────── -class NumberedCanvas(rl_canvas.Canvas): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._saved_page_states = [] - - def showPage(self): - self._saved_page_states.append(dict(self.__dict__)) - self._startPage() - - def save(self): - num_pages = len(self._saved_page_states) - for state in self._saved_page_states: - self.__dict__.update(state) - self._draw_page_decoration(num_pages) - super().showPage() - super().save() - - def _draw_page_decoration(self, page_count): - w, h = A4 - self.saveState() - - if self._pageNumber > 1: - # Header - self.setFillColor(C_BLUE) - self.rect(2.5*cm, h - 2*cm, w - 5*cm, 0.6*cm, fill=1, stroke=0) - self.setFillColor(colors.white) - self.setFont("Helvetica-Bold", 9) - self.drawString(2.8*cm, h - 1.65*cm, "Analisi Termica Forno") - self.setFont("Helvetica", 9) - self.drawRightString(w - 2.5*cm, h - 1.65*cm, "Cevolani") - - # Footer - self.setStrokeColor(C_BLUE_LIGHT) - self.setLineWidth(0.5) - self.line(2.5*cm, 1.8*cm, w - 2.5*cm, 1.8*cm) - self.setFillColor(C_BLUE) - self.setFont("Helvetica", 8) - self.drawString(2.5*cm, 1.3*cm, f"Report generato il {date.today().strftime('%d/%m/%Y')}") - self.drawRightString(w - 2.5*cm, 1.3*cm, - f"Pag. {self._pageNumber} / {page_count}") - self.restoreState() - - # ─── Genera PDF ─────────────────────────────────────────────────────────────── def generate_pdf(induttori: list): - out = str(BASE / "report_completo.pdf") - w, h = A4 - margin = 2.5*cm - frame_normal = Frame(margin, 2.5*cm, w - 2*margin, h - 5*cm, - id="normal", topPadding=0.8*cm) - frame_cover = Frame(margin, margin, w - 2*margin, h - 2*margin, - id="cover") + s = make_styles() + today_str = date.today().strftime("%d %B %Y") - doc = BaseDocTemplate(out, pagesize=A4) + frame_cover = Frame(MARGIN, MARGIN, CONTENT_W, H - 2*MARGIN, id="cover") + frame_normal = Frame(MARGIN, 2.5*cm, CONTENT_W, H - 2.5*cm - 1.6*cm, + id="normal", topPadding=0) + + doc = BaseDocTemplate(str(BASE / "report_completo.pdf"), pagesize=A4) doc.addPageTemplates([ PageTemplate(id="cover", frames=[frame_cover]), PageTemplate(id="normal", frames=[frame_normal]), ]) - styles = getSampleStyleSheet() - s_body = styles["BodyText"] - s_h1 = ParagraphStyle("H1", fontName="Helvetica-Bold", fontSize=14, - textColor=C_BLUE, spaceBefore=14, spaceAfter=6, - borderPad=4) - s_h2 = ParagraphStyle("H2", fontName="Helvetica-Bold", fontSize=11, - textColor=C_BLUE_LIGHT, spaceBefore=10, spaceAfter=4) - s_eq = ParagraphStyle("EQ", fontName="Courier", fontSize=10, - leftIndent=24, spaceBefore=4, spaceAfter=6, - backColor=C_ACCENT, borderPad=6) - s_cover_title = ParagraphStyle("CoverTitle", fontName="Helvetica-Bold", - fontSize=28, textColor=C_BLUE, - alignment=TA_CENTER, spaceAfter=10) - s_cover_sub = ParagraphStyle("CoverSub", fontName="Helvetica", - fontSize=14, textColor=C_BLUE_LIGHT, - alignment=TA_CENTER, spaceAfter=6) - s_cover_date = ParagraphStyle("CoverDate", fontName="Helvetica", - fontSize=11, textColor=colors.grey, - alignment=TA_CENTER) - story = [] - # ── Cover page ── - story.append(Spacer(1, 5*cm)) - story.append(HRFlowable(width="100%", thickness=3, color=C_BLUE)) - story.append(Spacer(1, 0.5*cm)) - story.append(Paragraph("Report Analisi Termica Forno", s_cover_title)) + # ── COVER PAGE ────────────────────────────────────────────────────────────── + # Barra superiore scura + story.append(Table( + [[Paragraph("CEVOLANI S.p.A.", + ParagraphStyle("ch", alignment=TA_LEFT)), + Paragraph("Analisi Termica — Uso Interno", + ParagraphStyle("cr", alignment=TA_LEFT))]], + colWidths=[CONTENT_W * 0.55, CONTENT_W * 0.45], + style=TableStyle([ + ("BACKGROUND", (0, 0), (-1, -1), C_DARK), + ("TOPPADDING", (0, 0), (-1, -1), 10), + ("BOTTOMPADDING", (0, 0), (-1, -1), 10), + ("LEFTPADDING", (0, 0), (-1, -1), 14), + ]) + )) + story.append(Spacer(1, 4*cm)) + + # Titolo principale + story.append(HRFlowable(width="100%", thickness=4, color=C_BLUE)) + story.append(Spacer(1, 0.4*cm)) + story.append(Paragraph( + "Report Analisi Termica Forno", + ParagraphStyle("ct", alignment=TA_CENTER, leading=32))) story.append(Spacer(1, 0.3*cm)) - story.append(Paragraph("Analisi IR – Cevolani", s_cover_sub)) - story.append(Spacer(1, 0.5*cm)) - story.append(HRFlowable(width="100%", thickness=3, color=C_BLUE)) - story.append(Spacer(1, 1*cm)) - story.append(Paragraph(f"Data: {date.today().strftime('%d %B %Y')}", s_cover_date)) - story.append(Paragraph(f"Induttori analizzati: {len(induttori)}", s_cover_date)) + story.append(Paragraph( + "Caratterizzazione termica degli induttori", + ParagraphStyle("cs", alignment=TA_CENTER, leading=18))) + story.append(Spacer(1, 0.4*cm)) + story.append(HRFlowable(width="100%", thickness=4, color=C_BLUE)) + story.append(Spacer(1, 2*cm)) + + # Scheda documento + doc_info = [ + ["Documento", "report_completo"], + ["Autore", AUTORE], + ["Data di emissione", today_str], + ["Versione", VERSIONE], + ["Stato", STATO], + ["Induttori analizzati", str(len(induttori))], + ] + story.append(Table(doc_info, colWidths=[5.5*cm, CONTENT_W - 5.5*cm], + style=COVER_INFO_STYLE)) story.append(NextPageTemplate("normal")) story.append(PageBreak()) - # ── Modello ── - story.append(Paragraph("Modello di fitting", s_h1)) - story.append(HRFlowable(width="100%", thickness=1, color=C_BLUE_LIGHT)) - story.append(Spacer(1, 0.2*cm)) + # ── MODELLO ───────────────────────────────────────────────────────────────── + story += section_header("1. Modello di fitting", s["h1"]) story.append(Paragraph( - "Ogni intervallo è interpolato con il modello di raffreddamento di Newton:", s_body)) - story.append(Paragraph("T(t) = T_inf + A · exp( -(t - t0) / τ )", s_eq)) + "Ogni intervallo di acquisizione è interpolato con la legge di " + "raffreddamento di Newton, che descrive lo scambio termico convettivo " + "tra il corpo e l'ambiente:", s["body"])) + story.append(Paragraph("T(t) = T_inf + A · exp( -(t - t₀) / τ )", s["eq"])) + story.append(Table([ + ["Simbolo", "Significato", "Unità"], + ["T_inf", "Temperatura ambiente (fissa)", "°C"], + ["A", "Ampiezza iniziale (libera)", "°C"], + ["t₀", "Istante di riferimento (fisso)", "s"], + ["τ", "Costante di tempo (libera)", "s"], + ], colWidths=[3*cm, 9*cm, 3*cm], style=TABLE_STD)) + story.append(Spacer(1, 0.3*cm)) story.append(Paragraph( - "dove T_inf = 22.99 °C è la temperatura ambiente media ponderata. " - "La regressione usa il metodo Trust Region Reflective (TRF) " - "di scipy.optimize.curve_fit.", s_body)) - story.append(Spacer(1, 0.5*cm)) + "La regressione non lineare è eseguita con il metodo " + "Trust Region Reflective (TRF) tramite " + "scipy.optimize.curve_fit. " + f"T_inf = 22.99 °C è la media ponderata sul tempo dell'intera acquisizione " + "(calcolata con la regola dei trapezi).", s["body"])) + story.append(Spacer(1, 0.4*cm)) - # ── Sezione per ogni induttore ── - for ind in induttori: - story.append(Paragraph(ind["nome"], s_h1)) - story.append(HRFlowable(width="100%", thickness=1, color=C_BLUE_LIGHT)) - story.append(Spacer(1, 0.2*cm)) + # ── SEZIONI INDUTTORI ──────────────────────────────────────────────────────── + for i, ind in enumerate(induttori, 1): + story += section_header(f"{i + 1}. {ind['nome']}", s["h1"]) - # Intervallo - story.append(Paragraph("Intervallo di campionamento", s_h2)) + # Campionamento + story.append(Paragraph("Intervallo di campionamento", s["h2"])) rows = [["Parametro", "Valore"], - ["Inizio", f"{ind['t0']} s"], - ["Fine", f"{ind['t_end']} s"], - ["Numero di punti", ind["n_punti"]]] + ["Inizio intervallo", f"{ind['t0']} s"], + ["Fine intervallo", f"{ind['t_end']} s"], + ["Numero di punti", ind["n_punti"]]] if ind["zona_exc"]: - rows.append(["Zona esclusa (peso = 0)", ind["zona_exc"]]) - story.append(Table(rows, colWidths=[8*cm, 6*cm], style=TABLE_HDR)) + rows.append(["Zona esclusa dal fit (σ → ∞)", ind["zona_exc"]]) + story.append(Table(rows, colWidths=[8*cm, CONTENT_W - 8*cm], + style=TABLE_STD)) story.append(Spacer(1, 0.3*cm)) # Equazione - story.append(Paragraph("Equazione trovata", s_h2)) - story.append(Paragraph(ind["equazione"], s_eq)) + story.append(Paragraph("Equazione del modello", s["h2"])) + story.append(Paragraph(ind["equazione"], s["eq"])) story.append(Spacer(1, 0.2*cm)) + story.append(Paragraph("Parametri stimati dalla regressione:", s["body"])) story.append(Table([ - ["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"], ""], - ], colWidths=[3.5*cm, 6*cm, 5*cm], style=TABLE_HDR)) + ["Parametro", "Valore stimato", "Incertezza (1σ)", "Note"], + ["A", f"{ind['A']} °C", f"± {ind['A_err']} °C", "Ampiezza"], + ["τ", f"{ind['tau']} s", f"± {ind['tau_err']} s","Costante di tempo"], + ["R²", ind["r2"], "—", "Qualità del fit"], + ], colWidths=[2.5*cm, 4.5*cm, 4*cm, CONTENT_W - 11*cm], + style=TABLE_STD)) story.append(Spacer(1, 0.3*cm)) - # Plot + # Grafico if ind["png"] and Path(ind["png"]).exists(): - story.append(Paragraph("Grafico", s_h2)) - story.append(Image(str(ind["png"]), width=14*cm, height=8.4*cm)) - story.append(Spacer(1, 0.4*cm)) + story.append(Paragraph("Grafico del fit", s["h2"])) + story.append(KeepTogether([ + Image(str(ind["png"]), width=CONTENT_W, height=CONTENT_W * 0.6), + Paragraph( + f"Figura {i}: fit esponenziale — {ind['nome']} " + f"[{ind['intervallo']}]", s["caption"]), + ])) + story.append(Spacer(1, 0.5*cm)) - # ── Riepilogo ── + # ── RIEPILOGO ──────────────────────────────────────────────────────────────── story.append(PageBreak()) - story.append(Paragraph("Riepilogo", s_h1)) - story.append(HRFlowable(width="100%", thickness=1, color=C_BLUE_LIGHT)) - story.append(Spacer(1, 0.3*cm)) + story += section_header(f"{len(induttori) + 2}. Riepilogo comparativo", s["h1"]) + story.append(Paragraph( + "Tabella comparativa dei parametri ottimali ottenuti dalla regressione " + "non lineare per ciascun induttore analizzato.", s["body"])) + story.append(Spacer(1, 0.2*cm)) + summary = [["Induttore", "Intervallo [s]", "A [°C]", "τ [s]", "R²"]] for ind in induttori: summary.append([ind["nome"], ind["intervallo"], ind["A"], ind["tau"], ind["r2"]]) story.append(Table(summary, - colWidths=[3.5*cm, 4.5*cm, 3*cm, 3*cm, 3*cm], - style=TABLE_SUMMARY)) + colWidths=[3.5*cm, 4.5*cm, 3*cm, 3*cm, CONTENT_W - 14*cm], + style=TABLE_DARK)) + story.append(Spacer(1, 0.5*cm)) + story.append(Paragraph( + "Note: tutti i fit usano T_inf = 22.99 °C fissa. " + "R² calcolato escludendo i punti con peso nullo.", s["body"])) - doc.build(story, canvasmaker=NumberedCanvas) + doc.build(story, canvasmaker=ProfessionalCanvas) print(f"report_completo.pdf generato ({len(induttori)} induttori).") diff --git a/report_completo.tex b/report_completo.tex index 7154ac3..702f4a7 100644 --- a/report_completo.tex +++ b/report_completo.tex @@ -6,18 +6,17 @@ \usepackage{booktabs} \usepackage{graphicx} \usepackage{float} -\usepackage{caption} \usepackage[table]{xcolor} \usepackage{hyperref} -\definecolor{hdrblue}{HTML}{1a3a5c} \usepackage{fancyhdr} -\pagestyle{fancy} -\fancyhf{} -\rhead{\textcolor{hdrblue}{\small Analisi Termica Forno}} -\lhead{\textcolor{hdrblue}{\small Cevolani}} +\definecolor{darkblue}{HTML}{1a3a5c} +\pagestyle{fancy}\fancyhf{} +\rhead{\textcolor{darkblue}{\small Cevolani S.p.A.}} +\lhead{\textcolor{darkblue}{\small Analisi Termica Forno}} \rfoot{\thepage} -\title{\textbf{Report Analisi Termica Forno}} -\author{Analisi IR -- Cevolani} +\lfoot{\textcolor{gray}{\tiny Ver. 1.0 -- 14/05/2026}} +\title{\textbf{Report Analisi Termica Forno}\\[0.5em]\large Caratterizzazione termica degli induttori} +\author{Ufficio Tecnico – Cevolani S.p.A.} \date{\today} \begin{document} \maketitle @@ -29,121 +28,94 @@ Ogni intervallo è interpolato con il modello di raffreddamento di Newton: \begin{equation} T(t) = T_\infty + A \cdot e^{-(t-t_0)/\tau} \end{equation} -dove $T_\infty = 22.99\,^\circ\mathrm{C}$ è la temperatura ambiente media ponderata. +dove $T_\infty = 22{,}99\,^\circ\mathrm{C}$ è la temperatura ambiente media ponderata. La regressione non lineare usa il metodo \textit{Trust Region Reflective} (TRF) di \texttt{scipy.optimize.curve\_fit}. \section{Report Induttore 1} \subsection{Intervallo di campionamento} -\begin{tabular}{ll} -\toprule -\textbf{Parametro} & \textbf{Valore} \\ -\midrule +\begin{tabular}{ll}\toprule +\textbf{Parametro} & \textbf{Valore} \\\midrule Inizio & $110.81$ s \\ Fine & $111.56$ s \\ Numero di punti & 5 \\ -\bottomrule -\end{tabular} +\bottomrule\end{tabular} \subsection{Equazione trovata} \begin{equation} T(t) = 22.99 + 87.3140 \cdot e^{-(t - 110.81) / 15.7231} \end{equation} -\begin{tabular}{lll} -\toprule -\textbf{Parametro} & \textbf{Valore} & \textbf{Incertezza (1$\sigma$)} \\ -\midrule +\begin{tabular}{lll}\toprule +\textbf{Param.} & \textbf{Valore} & \textbf{Incertezza ($1\sigma$)} \\\midrule $A$ & $87.3140\,^\circ\mathrm{C}$ & $\pm\,0.0260\,^\circ\mathrm{C}$ \\ $\tau$ & $15.7231\,\mathrm{s}$ & $\pm\,0.2021\,\mathrm{s}$ \\ $R^2$ & \multicolumn{2}{l}{0.999505} \\ -\bottomrule -\end{tabular} +\bottomrule\end{tabular} \subsection{Grafico} -\begin{figure}[H] -\centering +\begin{figure}[H]\centering \includegraphics[width=0.9\textwidth]{C:\Users\davide.grilli\Desktop\vs\temperatura\induttore1\induttore1.png} \caption{Fit esponenziale -- Report Induttore 1 [110.81 – 111.56 s]} -\end{figure} -\newpage +\end{figure}\newpage \section{Report Induttore 2} \subsection{Intervallo di campionamento} -\begin{tabular}{ll} -\toprule -\textbf{Parametro} & \textbf{Valore} \\ -\midrule +\begin{tabular}{ll}\toprule +\textbf{Parametro} & \textbf{Valore} \\\midrule Inizio & $112.02$ s \\ Fine & $113.95$ s \\ Numero di punti & 12 \\ -\bottomrule -\end{tabular} +\bottomrule\end{tabular} \subsection{Equazione trovata} \begin{equation} T(t) = 22.99 + 158.3430 \cdot e^{-(t - 112.02) / 13.2844} \end{equation} -\begin{tabular}{lll} -\toprule -\textbf{Parametro} & \textbf{Valore} & \textbf{Incertezza (1$\sigma$)} \\ -\midrule +\begin{tabular}{lll}\toprule +\textbf{Param.} & \textbf{Valore} & \textbf{Incertezza ($1\sigma$)} \\\midrule $A$ & $158.3430\,^\circ\mathrm{C}$ & $\pm\,0.2088\,^\circ\mathrm{C}$ \\ $\tau$ & $13.2844\,\mathrm{s}$ & $\pm\,0.2183\,\mathrm{s}$ \\ $R^2$ & \multicolumn{2}{l}{0.997314} \\ -\bottomrule -\end{tabular} +\bottomrule\end{tabular} \subsection{Grafico} -\begin{figure}[H] -\centering +\begin{figure}[H]\centering \includegraphics[width=0.9\textwidth]{C:\Users\davide.grilli\Desktop\vs\temperatura\induttore2\induttore2.png} \caption{Fit esponenziale -- Report Induttore 2 [112.02 – 113.95 s]} -\end{figure} -\newpage +\end{figure}\newpage \section{Report Induttore 3} \subsection{Intervallo di campionamento} -\begin{tabular}{ll} -\toprule -\textbf{Parametro} & \textbf{Valore} \\ -\midrule +\begin{tabular}{ll}\toprule +\textbf{Parametro} & \textbf{Valore} \\\midrule Inizio & $115.0$ s \\ Fine & $133.7$ s \\ Numero di punti & 125 \\ -\bottomrule -\end{tabular} +\bottomrule\end{tabular} \subsection{Equazione trovata} \begin{equation} T(t) = 22.99 + 185.1847 \cdot e^{-(t - 115.0) / 16.2654} \end{equation} -\begin{tabular}{lll} -\toprule -\textbf{Parametro} & \textbf{Valore} & \textbf{Incertezza (1$\sigma$)} \\ -\midrule +\begin{tabular}{lll}\toprule +\textbf{Param.} & \textbf{Valore} & \textbf{Incertezza ($1\sigma$)} \\\midrule $A$ & $185.1847\,^\circ\mathrm{C}$ & $\pm\,0.2706\,^\circ\mathrm{C}$ \\ $\tau$ & $16.2654\,\mathrm{s}$ & $\pm\,0.0464\,\mathrm{s}$ \\ $R^2$ & \multicolumn{2}{l}{0.993827} \\ -\bottomrule -\end{tabular} +\bottomrule\end{tabular} \subsection{Grafico} -\begin{figure}[H] -\centering +\begin{figure}[H]\centering \includegraphics[width=0.9\textwidth]{C:\Users\davide.grilli\Desktop\vs\temperatura\induttore3\induttore3.png} \caption{Fit esponenziale -- Report Induttore 3 [115.0 – 133.7 s]} -\end{figure} -\newpage +\end{figure}\newpage \section{Riepilogo} -\begin{tabular}{lllll} -\toprule -\textbf{Induttore} & \textbf{Intervallo [s]} & \textbf{A [°C]} & \textbf{$\tau$ [s]} & \textbf{$R^2$} \\ -\midrule +\begin{tabular}{lllll}\toprule +\textbf{Induttore} & \textbf{Intervallo [s]} & \textbf{A [°C]} & \textbf{$\tau$ [s]} & \textbf{$R^2$} \\\midrule Report Induttore 1 & 110.81 – 111.56 s & 87.3140 & 15.7231 & 0.999505 \\ Report Induttore 2 & 112.02 – 113.95 s & 158.3430 & 13.2844 & 0.997314 \\ Report Induttore 3 & 115.0 – 133.7 s & 185.1847 & 16.2654 & 0.993827 \\ -\bottomrule -\end{tabular} +\bottomrule\end{tabular} \end{document} \ No newline at end of file