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