report_completo: identità aziendale, math LaTeX, T∞ dinamica

- Costanti aziendali: SOCIETA, DIPARTIMENTO, DOC_NUM, REVISIONE, CLASSIFICAZIONE
- Copertina con scheda documento (codice, revisione, stato, classificazione)
- Header a due righe: azienda | titolo | doc+rev / dipartimento | data
- Footer: classificazione | riferimento documento | pagina X/Y
- render_eq e render_sym con altezza dinamica (ImageReader)
- Simboli matematici nelle tabelle via render_sym (matplotlib mathtext)
- T∞ letta dinamicamente da start/report.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 4.6 committed 2026-05-14 11:01:44 +02:00
1 parent bccec166e4
commit 6e5e08d82f
2 files changed
+499 -382

No files matched your search

+452 -320
View File
@@ -1,119 +1,255 @@
"""
report_completo.py
Genera report_completo.tex e report_completo.pdf leggendo dinamicamente
tutti i report*.md presenti nelle cartelle induttore*/.
"""
import re
import tempfile
import shutil
from pathlib import Path
from datetime import date
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
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_LEFT
from reportlab.platypus import (BaseDocTemplate, PageTemplate, Frame,
Paragraph, Spacer, Table, TableStyle,
Image, HRFlowable, PageBreak, NextPageTemplate,
KeepTogether)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import (
BaseDocTemplate, PageTemplate, Frame,
Paragraph, Spacer, Table, TableStyle,
Image, HRFlowable, PageBreak, NextPageTemplate, KeepTogether,
)
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas as rl_canvas
from datetime import date
BASE = Path(__file__).parent
# ── Identità aziendale ────────────────────────────────────────────────────────
SOCIETA = "Cevolani Italia s.r.l."
DIPARTIMENTO = "Ufficio Tecnico Meccanico"
DOC_NUM = "RT-TERM-001"
REVISIONE = "Rev. 00"
STATO = "Definitivo"
CLASSIFICAZIONE = "USO INTERNO"
TODAY = date.today()
AUTORE = "Ufficio Tecnico – Cevolani S.p.A."
VERSIONE = "1.0"
STATO = "Definitivo"
# ─── 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
W, H = A4
BASE = Path(__file__).parent
W, H = A4
MARGIN = 2.5 * cm
CONTENT_W = W - 2 * MARGIN
CW = 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"),
])
# ── Palette ───────────────────────────────────────────────────────────────────
C_DARK = colors.HexColor("#0f2236")
C_BLUE = colors.HexColor("#1a3a5c")
C_MID = colors.HexColor("#2c5f8a")
C_LIGHT = colors.HexColor("#4a8fc0")
C_GRAY = colors.HexColor("#f6f7f8")
C_BORDER = colors.HexColor("#b8c8d8")
C_WHITE = colors.white
C_TEXT = colors.HexColor("#1e1e1e")
TABLE_STD = make_table_style()
TABLE_DARK = make_table_style(C_DARK)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 1. PARSING DEI REPORT MARKDOWN
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
("LEFTPADDING", (0, 0), (-1, -1), 12),
])
def _strip(val: str, *suffixes) -> str:
for s in suffixes:
val = val.replace(s, "")
return val.strip()
# ─── Parser markdown ──────────────────────────────────────────────────────────
def parse_md(md_path: Path) -> dict:
def parse_report(md_path: Path) -> dict:
text = md_path.read_text(encoding="utf-8")
data = {}
m = re.search(r"^# (.+)$", text, re.MULTILINE)
data["nome"] = m.group(1).strip() if m else md_path.parent.name
title = re.search(r"^# (.+)$", text, re.MULTILINE)
kv2 = {k.strip(): v.strip()
for k, v in re.findall(r"^\| (.+?) \| (.+?) \|$", text, re.MULTILINE)
if k.strip() != "---"}
kv3 = {r[0].strip(): (r[1].strip(), r[2].strip())
for r in re.findall(r"^\| (.+?) \| (.+?) \| (.+?) \|$", text, re.MULTILINE)
if r[0].strip() not in ("Parametro", "---")}
eq = re.search(r"\$\$(.+?)\$\$", text, re.DOTALL)
kv = dict(re.findall(r"^\| (.+?) \| (.+?) \|$", text, re.MULTILINE))
kv = {k.strip(): v.strip() for k, v in kv.items() if k.strip() != "---"}
t0 = _strip(kv2.get("Inizio", ""), " s")
tend = _strip(kv2.get("Fine", ""), " s")
data["t0"] = kv.get("Inizio", "").replace(" s", "")
data["t_end"] = kv.get("Fine", "").replace(" s", "")
data["n_punti"] = kv.get("Numero di punti", "")
data["zona_exc"] = kv.get("Zona esclusa (peso = 0)", None)
data["r2"] = kv.get("R²", "")
data["intervallo"] = f"{data['t0']} – {data['t_end']} s"
def param(key):
v, e = kv3.get(key, ("", ""))
return (_strip(v, " °C", " s"),
_strip(e, "± ", "+/- ", " °C", " s"))
m = re.search(r"\$\$(.+?)\$\$", text, re.DOTALL)
data["equazione_latex"] = m.group(1).strip() if m else ""
data["equazione"] = (data["equazione_latex"]
.replace(r"\cdot", "·").replace(r"\infty", "∞"))
A, A_err = param("A")
tau, tau_err = param("τ")
rows3 = re.findall(r"^\| (.+?) \| (.+?) \| (.+?) \|$", text, re.MULTILINE)
params = {r[0].strip(): (r[1].strip(), r[2].strip())
for r in rows3 if r[0].strip() not in ("Parametro", "---")}
data["A"] = params.get("A", ("", ""))[0].replace(" °C", "")
data["A_err"] = (params.get("A", ("", ""))[1]
.replace("± ", "").replace("+/- ", "").replace(" °C", ""))
data["tau"] = params.get("τ", ("", ""))[0].replace(" s", "")
data["tau_err"] = (params.get("τ", ("", ""))[1]
.replace("± ", "").replace("+/- ", "").replace(" s", ""))
pngs = sorted(md_path.parent.glob("*.png"))
data["png"] = pngs[0] if pngs else None
return data
return {
"nome": title.group(1).strip() if title else md_path.parent.name,
"t0": t0,
"t_end": tend,
"intervallo": f"{t0} – {tend} s",
"n_punti": kv2.get("Numero di punti", ""),
"zona_exc": kv2.get("Zona esclusa (peso = 0)"),
"eq_latex": eq.group(1).strip() if eq else "",
"A": A, "A_err": A_err,
"tau": tau, "tau_err": tau_err,
"r2": kv2.get("R²", ""),
"png": next(iter(sorted(md_path.parent.glob("*.png"))), None),
}
def collect_induttori() -> list:
return [parse_md(md)
for folder in sorted(BASE.glob("induttore*/"))
for md in sorted(folder.glob("report*.md"))]
def collect_induttori() -> list[dict]:
return [
parse_report(md)
for folder in sorted(BASE.glob("induttore*/"))
for md in sorted(folder.glob("report*.md"))
]
# ─── Canvas con header/footer ─────────────────────────────────────────────────
def read_t_inf() -> str:
"""Legge T∞ da start/report.md generato da calcola_t_amb.py."""
report = BASE / "start" / "report.md"
if not report.exists():
return "N/D"
m = re.search(r"media ponderata\*\*\s*\|\s*\*\*([0-9.]+)\s*°C",
report.read_text(encoding="utf-8"))
return m.group(1) if m else "N/D"
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 2. RENDERING EQUAZIONI CON MATPLOTLIB
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _save_math_png(latex: str, tmpdir: Path, prefix: str,
figsize, fontsize: int, dpi: int, pad: float) -> Path:
"""Renderizza una stringa LaTeX math come PNG trasparente."""
fig = plt.figure(figsize=figsize)
ax = fig.add_axes([0, 0, 1, 1])
ax.text(0.5, 0.5, f"${latex}$",
ha="center", va="center", fontsize=fontsize,
transform=ax.transAxes, color="#0f2236")
ax.axis("off")
out = tmpdir / f"{prefix}_{abs(hash(latex))}.png"
fig.savefig(out, dpi=dpi, bbox_inches="tight",
transparent=True, pad_inches=pad)
plt.close(fig)
return out
def render_eq(latex: str, tmpdir: Path,
width_cm: float = 13, fontsize: int = 16) -> Image:
"""Equazione a blocco, larghezza piena, altezza proporzionale al PNG."""
out = _save_math_png(latex, tmpdir, "eq",
figsize=(width_cm / 2.54, 1.5),
fontsize=fontsize, dpi=220, pad=0.25)
iw, ih = ImageReader(str(out)).getSize()
w_pt = width_cm * cm
return Image(str(out), width=w_pt, height=w_pt * ih / iw)
def render_sym(latex: str, tmpdir: Path, width_cm: float = 1.3) -> Image:
"""Simbolo matematico per cella tabella — stesso stile di render_eq."""
out = _save_math_png(latex, tmpdir, "sym",
figsize=(width_cm / 2.54, 0.6),
fontsize=13, dpi=220, pad=0.12)
iw, ih = ImageReader(str(out)).getSize()
w_pt = width_cm * cm
return Image(str(out), width=w_pt, height=w_pt * ih / iw)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 3. GENERAZIONE .TEX
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def generate_tex(induttori: list[dict], t_inf: str):
today_str = TODAY.strftime("%d/%m/%Y")
lines = [
r"\documentclass[11pt,a4paper]{article}",
r"\usepackage[utf8]{inputenc}",
r"\usepackage[T1]{fontenc}",
r"\usepackage[margin=2.5cm]{geometry}",
r"\usepackage{amsmath,amssymb,booktabs,graphicx,float}",
r"\usepackage[table]{xcolor}",
r"\usepackage[colorlinks,linkcolor=blue,urlcolor=blue]{hyperref}",
r"\usepackage{fancyhdr}",
r"\definecolor{darkblue}{HTML}{1a3a5c}",
r"\pagestyle{fancy}\fancyhf{}",
r"\lhead{\textcolor{darkblue}{\small\textbf{" + SOCIETA + r"}}}",
r"\chead{\textcolor{darkblue}{\small Analisi Termica Forno}}",
r"\rhead{\textcolor{darkblue}{\small " + DOC_NUM + r"\ --\ " + REVISIONE + r"}}",
r"\lfoot{\textcolor{gray}{\tiny " + CLASSIFICAZIONE + r"}}",
r"\cfoot{\textcolor{gray}{\tiny " + DOC_NUM + r"\ |\ " + REVISIONE + r"\ |\ " + today_str + r"}}",
r"\rfoot{\thepage}",
rf"\title{{\textbf{{Report Analisi Termica Forno}}\\[0.4em]"
rf"\large Caratterizzazione termica degli induttori}}",
rf"\author{{{DIPARTIMENTO} \\ {SOCIETA}}}",
r"\date{\today}",
r"\begin{document}",
r"\maketitle\tableofcontents\newpage",
"",
r"\section{Modello di fitting}",
r"Ogni intervallo è interpolato con la legge di raffreddamento di Newton:",
r"\begin{equation}",
r" T(t) = T_{\infty} + A\,e^{-(t-t_0)/\tau}",
r"\end{equation}",
rf"con $T_{{\infty}}={t_inf.replace('.', '{,}')}\,^{{\circ}}\mathrm{{C}}$ fissa (media ponderata),"
r" parametri $A$ e $\tau$ liberi stimati con TRF (\texttt{scipy.optimize.curve\_fit}).",
"",
]
for ind in induttori:
lines += [
rf"\section{{{ind['nome']}}}",
r"\subsection{Intervallo di campionamento}",
r"\begin{tabular}{ll}\toprule",
r"\textbf{Parametro}&\textbf{Valore}\\\midrule",
rf"Inizio&${ind['t0']}$ s\\",
rf"Fine&${ind['t_end']}$ s\\",
rf"N.\,punti&{ind['n_punti']}\\",
]
if ind["zona_exc"]:
lines.append(rf"Zona esclusa&{ind['zona_exc']}\\")
lines += [
r"\bottomrule\end{tabular}",
r"\subsection{Equazione trovata}",
r"\begin{equation}", f" {ind['eq_latex']}", r"\end{equation}",
r"\begin{tabular}{lll}\toprule",
r"\textbf{Param.}&\textbf{Valore}&\textbf{Incert.\,($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\end{tabular}",
r"\subsection{Grafico}",
r"\begin{figure}[H]\centering",
rf"\includegraphics[width=.9\textwidth]{{{ind['png']}}}",
rf"\caption{{Fit esponenziale — {ind['nome']} [{ind['intervallo']}]}}",
r"\end{figure}\newpage",
]
lines += [
r"\section{Riepilogo}",
r"\begin{tabular}{lllll}\toprule",
r"\textbf{Induttore}&\textbf{Intervallo [s]}&\textbf{$A$ [°C]}"
r"&\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\end{tabular}", r"\end{document}"]
(BASE / "report_completo.tex").write_text("\n".join(lines), encoding="utf-8")
print("report_completo.tex generato.")
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 4. CANVAS CON HEADER / FOOTER
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Altezza header: riga 1 (azienda / titolo / docnum) + riga 2 (dip. / data)
HDR_H = 1.6 * cm
class ProfessionalCanvas(rl_canvas.Canvas):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -135,160 +271,198 @@ class ProfessionalCanvas(rl_canvas.Canvas):
def _draw_header(self):
self.saveState()
# Barra colorata in cima
# Sfondo intero header
self.setFillColor(C_DARK)
self.rect(0, H - 1.4*cm, W, 1.4*cm, fill=1, stroke=0)
self.rect(0, H - HDR_H, W, HDR_H, fill=1, stroke=0)
# Striscia laterale colorata
self.setFillColor(C_LIGHT)
self.rect(0, H - 1.4*cm, 0.5*cm, 1.4*cm, fill=1, stroke=0)
self.rect(0, H - HDR_H, 0.4 * cm, HDR_H, fill=1, stroke=0)
# Linea separatrice tra riga 1 e riga 2
self.setStrokeColor(colors.HexColor("#1e4a72"))
self.setLineWidth(0.4)
self.line(0.6 * cm, H - HDR_H * 0.55, W - 0.5 * cm, H - HDR_H * 0.55)
# -- Riga 1: azienda | titolo documento | codice + revisione --
y1 = H - HDR_H * 0.38
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.setFont("Helvetica-Bold", 8)
self.drawString(0.65 * cm, y1, SOCIETA.upper())
self.setFont("Helvetica", 7.5)
self.drawCentredString(W / 2, y1, "ANALISI TERMICA FORNO")
self.setFont("Helvetica-Bold", 7.5)
self.drawRightString(W - 0.5 * cm, y1, f"{DOC_NUM} | {REVISIONE}")
# -- Riga 2: dipartimento | data --
y2 = H - HDR_H * 0.75
self.setFillColor(colors.HexColor("#8ab4d0"))
self.setFont("Helvetica", 6.5)
self.drawString(0.65 * cm, y2, DIPARTIMENTO)
self.drawRightString(W - 0.5 * cm, y2, TODAY.strftime("%d/%m/%Y"))
self.restoreState()
def _draw_footer(self, total):
self.saveState()
# Linea superiore footer
self.setStrokeColor(C_MID)
self.setLineWidth(0.8)
self.line(MARGIN, 1.9*cm, W - MARGIN, 1.9*cm)
self.setLineWidth(0.6)
self.line(MARGIN, 1.9 * cm, W - MARGIN, 1.9 * cm)
# Classificazione (sinistra)
self.setFillColor(C_BLUE)
self.setFont("Helvetica-Bold", 7)
self.drawString(MARGIN, 1.5 * cm, CLASSIFICAZIONE)
# Riferimento documento (centro)
self.setFillColor(C_MID)
self.setFont("Helvetica", 7)
self.drawCentredString(
W / 2, 1.5 * cm,
f"{DOC_NUM} | {REVISIONE} | {STATO} | {TODAY.strftime('%d/%m/%Y')}"
)
# Numero pagina (destra)
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,
self.drawRightString(W - MARGIN, 1.5 * 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
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 5. COMPONENTI PDF
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _styles() -> dict:
base = getSampleStyleSheet()
mono = ParagraphStyle("mono", fontName="Courier", fontSize=9, textColor=C_TEXT)
return {
"body": ParagraphStyle("body", parent=base["BodyText"],
fontSize=9.5, leading=14.5, textColor=C_TEXT,
spaceAfter=6),
"mono": mono,
"h1": ParagraphStyle("h1", fontName="Helvetica-Bold", fontSize=12,
textColor=C_WHITE, leading=16, spaceAfter=2),
"h2": ParagraphStyle("h2", fontName="Helvetica-Bold", fontSize=10,
textColor=C_BLUE, spaceBefore=10, spaceAfter=4,
leading=13),
"cap": ParagraphStyle("cap", fontName="Helvetica-Oblique", fontSize=8,
textColor=colors.HexColor("#555555"),
alignment=TA_CENTER, spaceBefore=2),
"cov_title": ParagraphStyle("cov_title", fontName="Helvetica-Bold",
fontSize=24, textColor=C_BLUE,
alignment=TA_CENTER, leading=30),
"cov_sub": ParagraphStyle("cov_sub", fontName="Helvetica",
fontSize=12, textColor=C_MID,
alignment=TA_CENTER, leading=17),
}
def section_header(title: str, s_h1) -> list:
"""Barra blu con titolo sezione."""
tbl = Table([[Paragraph(title, s_h1)]], colWidths=[CONTENT_W])
def _ts(hdr_color=None) -> TableStyle:
hc = hdr_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), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 7),
("RIGHTPADDING", (0, 0), (-1, -1), 7),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
])
def _ts_cover() -> TableStyle:
"""Stile per la scheda documento in copertina."""
return TableStyle([
("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
("FONTNAME", (1, 0), (1, -1), "Helvetica"),
("FONTSIZE", (0, 0), (-1, -1), 10),
("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.5, C_BORDER),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("LEFTPADDING", (0, 0), (-1, -1), 14),
("RIGHTPADDING", (0, 0), (-1, -1), 14),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
# Riga classificazione in evidenza
("BACKGROUND", (0, 5), (-1, 5), colors.HexColor("#fff3cd")),
("TEXTCOLOR", (1, 5), (1, 5), colors.HexColor("#7a4f00")),
("FONTNAME", (1, 5), (1, 5), "Helvetica-Bold"),
])
def _section_banner(title: str, s_h1) -> list:
tbl = Table([[Paragraph(title, s_h1)]], colWidths=[CW])
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),
("BACKGROUND", (0, 0), (-1, -1), C_BLUE),
("LEFTPADDING", (0, 0), (-1, -1), 12),
("RIGHTPADDING", (0, 0), (-1, -1), 12),
("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
]))
return [tbl, Spacer(1, 0.25*cm)]
return [tbl, Spacer(1, 0.2 * cm)]
# ─── Genera .tex ──────────────────────────────────────────────────────────────
def generate_tex(induttori: list):
today = date.today().strftime("%d/%m/%Y")
lines = [
r"\documentclass[11pt,a4paper]{article}",
r"\usepackage[utf8]{inputenc}",
r"\usepackage[T1]{fontenc}",
r"\usepackage[margin=2.5cm]{geometry}",
r"\usepackage{amsmath,amssymb}",
r"\usepackage{booktabs}",
r"\usepackage{graphicx}",
r"\usepackage{float}",
r"\usepackage[table]{xcolor}",
r"\usepackage{hyperref}",
r"\usepackage{fancyhdr}",
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"\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",
r"\tableofcontents",
r"\newpage",
"",
r"\section{Modello di fitting}",
r"Ogni intervallo è interpolato con il modello di raffreddamento di Newton:",
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"La regressione non lineare usa il metodo \textit{Trust Region Reflective} (TRF)",
r"di \texttt{scipy.optimize.curve\_fit}.",
"",
def _cover(s: dict, n: int) -> list:
# Intestazione aziendale
banner = Table([[
Paragraph(
f"<font color='white' size='11'><b>{SOCIETA.upper()}</b></font>",
ParagraphStyle("_bl", alignment=TA_LEFT)),
Paragraph(
f"<font color='#4a8fc0' size='9'>{DIPARTIMENTO}</font>",
ParagraphStyle("_br", alignment=TA_RIGHT)),
]], colWidths=[CW * 0.6, CW * 0.4],
style=TableStyle([
("BACKGROUND", (0, 0), (-1, -1), C_DARK),
("TOPPADDING", (0, 0), (-1, -1), 14),
("BOTTOMPADDING", (0, 0), (-1, -1), 14),
("LEFTPADDING", (0, 0), (-1, -1), 16),
("RIGHTPADDING", (0, 0), (-1, -1), 16),
]))
# Scheda documento
doc_table = Table([
["Codice documento", DOC_NUM],
["Revisione", REVISIONE],
["Data di emissione", TODAY.strftime("%d %B %Y")],
["Stato", STATO],
["Redatto da", DIPARTIMENTO],
["Classificazione", CLASSIFICAZIONE],
["Induttori analizzati", str(n)],
], colWidths=[6 * cm, CW - 6 * cm], style=_ts_cover())
return [
banner,
Spacer(1, 3.8 * cm),
HRFlowable(width="100%", thickness=5, color=C_BLUE),
Spacer(1, 0.45 * cm),
Paragraph("Report Analisi Termica Forno", s["cov_title"]),
Spacer(1, 0.25 * cm),
Paragraph("Caratterizzazione termica degli induttori", s["cov_sub"]),
Spacer(1, 0.45 * cm),
HRFlowable(width="100%", thickness=5, color=C_BLUE),
Spacer(1, 1.8 * cm),
doc_table,
NextPageTemplate("normal"),
PageBreak(),
]
for ind in induttori:
lines += [
rf"\section{{{ind['nome']}}}",
r"\subsection{Intervallo di campionamento}",
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\end{tabular}", "",
r"\subsection{Equazione trovata}",
r"\begin{equation}",
f" {ind['equazione_latex']}",
r"\end{equation}",
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\end{tabular}", "",
r"\subsection{Grafico}",
r"\begin{figure}[H]\centering",
rf"\includegraphics[width=0.9\textwidth]{{{ind['png']}}}",
rf"\caption{{Fit esponenziale -- {ind['nome']} [{ind['intervallo']}]}}",
r"\end{figure}\newpage", "",
]
lines += [
r"\section{Riepilogo}",
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\end{tabular}", "", r"\end{document}"]
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 6. GENERAZIONE PDF
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
(BASE / "report_completo.tex").write_text("\n".join(lines), encoding="utf-8")
print("report_completo.tex generato.")
def generate_pdf(induttori: list[dict], t_inf: str):
s = _styles()
tmpdir = Path(tempfile.mkdtemp())
# ─── Genera PDF ───────────────────────────────────────────────────────────────
def generate_pdf(induttori: list):
s = make_styles()
today_str = date.today().strftime("%d %B %Y")
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,
frame_cover = Frame(MARGIN, MARGIN, CW, H - 2 * MARGIN, id="cover")
frame_normal = Frame(MARGIN, 2.6 * cm, CW, H - HDR_H - 2.6 * cm - 0.3 * cm,
id="normal", topPadding=0)
doc = BaseDocTemplate(str(BASE / "report_completo.pdf"), pagesize=A4)
@@ -297,79 +471,39 @@ def generate_pdf(induttori: list):
PageTemplate(id="normal", frames=[frame_normal]),
])
story = []
story: list = []
# ── COVER PAGE ──────────────────────────────────────────────────────────────
# Barra superiore scura
story.append(Table(
[[Paragraph("<font color='white' size='11'><b>CEVOLANI S.p.A.</b></font>",
ParagraphStyle("ch", alignment=TA_LEFT)),
Paragraph("<font color='#4a8fc0' size='9'>Analisi Termica — Uso Interno</font>",
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))
# ── Copertina ──
story += _cover(s, len(induttori))
# Titolo principale
story.append(HRFlowable(width="100%", thickness=4, color=C_BLUE))
story.append(Spacer(1, 0.4*cm))
# ── 1. Modello di fitting ──
story += _section_banner("1. Modello di fitting", s["h1"])
story.append(Paragraph(
"<font color='#1a3a5c' size='26'><b>Report Analisi Termica Forno</b></font>",
ParagraphStyle("ct", alignment=TA_CENTER, leading=32)))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"<font color='#2c5f8a' size='13'>Caratterizzazione termica degli induttori</font>",
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 += section_header("1. Modello di fitting", s["h1"])
story.append(Paragraph(
"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"]))
"Ogni tratto di acquisizione è interpolato con la legge di "
"raffreddamento di Newton (convezione termica):", s["body"]))
story.append(Spacer(1, 0.1 * cm))
story.append(render_eq(r"T(t) = T_{\infty} + A\,e^{-(t-t_0)/\tau}", tmpdir))
story.append(Spacer(1, 0.15 * cm))
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))
["Simbolo", "Significato", "Unità"],
[render_sym(r"T_{\infty}", tmpdir), "Temperatura ambiente — valore fisso", "°C"],
[render_sym(r"A", tmpdir), "Ampiezza iniziale — parametro libero", "°C"],
[render_sym(r"t_0", tmpdir), "Istante di riferimento — valore fisso", "s"],
[render_sym(r"\tau", tmpdir), "Costante di tempo — parametro libero", "s"],
], colWidths=[2.5 * cm, 9.5 * cm, CW - 12 * cm], style=_ts()))
story.append(Spacer(1, 0.3 * cm))
story.append(Paragraph(
"La regressione non lineare è eseguita con il metodo "
"<i>Trust Region Reflective</i> (TRF) tramite "
"<font name='Courier'>scipy.optimize.curve_fit</font>. "
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))
"La regressione usa il metodo <i>Trust Region Reflective</i> (TRF) "
"(<font name='Courier'>scipy.optimize.curve_fit</font>). "
f"T∞ = {t_inf} °C è la media ponderata sul tempo dell'intera "
"acquisizione (regola dei trapezi, "
"<font name='Courier'>calcola_t_amb.py</font>).",
s["body"]))
story.append(Spacer(1, 0.4 * cm))
# ── SEZIONI INDUTTORI ────────────────────────────────────────────────────────
for i, ind in enumerate(induttori, 1):
story += section_header(f"{i + 1}. {ind['nome']}", s["h1"])
# ── Sezioni induttori ──
for i, ind in enumerate(induttori, 2):
story += _section_banner(f"{i}. {ind['nome']}", s["h1"])
# Campionamento
story.append(Paragraph("Intervallo di campionamento", s["h2"]))
@@ -378,65 +512,63 @@ def generate_pdf(induttori: list):
["Fine intervallo", f"{ind['t_end']} s"],
["Numero di punti", ind["n_punti"]]]
if ind["zona_exc"]:
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))
rows.append(["Zona esclusa (σ → ∞)", ind["zona_exc"]])
story.append(Table(rows, colWidths=[7 * cm, CW - 7 * cm], style=_ts()))
story.append(Spacer(1, 0.3 * cm))
# Equazione
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(render_eq(ind["eq_latex"], tmpdir))
story.append(Spacer(1, 0.2 * cm))
story.append(Table([
["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))
["Parametro", "Valore stimato", "Incertezza (1σ)", "Descrizione"],
[render_sym(r"A", tmpdir), f"{ind['A']} °C", f"± {ind['A_err']} °C", "Ampiezza"],
[render_sym(r"\tau", tmpdir), f"{ind['tau']} s", f"± {ind['tau_err']} s", "Costante di tempo"],
[render_sym(r"R^2", tmpdir), ind["r2"], "—", "Qualità del fit"],
], colWidths=[2.2 * cm, 4.2 * cm, 4 * cm, CW - 10.4 * cm], style=_ts()))
story.append(Spacer(1, 0.3 * cm))
# Grafico
if ind["png"] and Path(ind["png"]).exists():
story.append(Paragraph("Grafico del fit", s["h2"]))
story.append(KeepTogether([
Image(str(ind["png"]), width=CONTENT_W, height=CONTENT_W * 0.6),
Image(str(ind["png"]), width=CW, height=CW * 0.6),
Paragraph(
f"Figura {i}: fit esponenziale — {ind['nome']} "
f"[{ind['intervallo']}]", s["caption"]),
f"Fig. {i - 1} — Fit esponenziale: {ind['nome']} "
f"[{ind['intervallo']}]", s["cap"]),
]))
story.append(Spacer(1, 0.5*cm))
story.append(Spacer(1, 0.5 * cm))
# ── RIEPILOGO ────────────────────────────────────────────────────────────────
# ── Riepilogo ──
story.append(PageBreak())
story += section_header(f"{len(induttori) + 2}. Riepilogo comparativo", s["h1"])
n_sec = len(induttori) + 2
story += _section_banner(f"{n_sec}. 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))
"Parametri ottimali dalla regressione non lineare per ciascun induttore. "
"R² calcolato escludendo i punti con peso nullo.", 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, CONTENT_W - 14*cm],
style=TABLE_DARK))
story.append(Spacer(1, 0.5*cm))
story.append(Paragraph(
"<b>Note:</b> tutti i fit usano T_inf = 22.99 °C fissa. "
"R² calcolato escludendo i punti con peso nullo.", s["body"]))
colWidths=[3.5 * cm, 4.5 * cm, 3 * cm, 3 * cm, CW - 14 * cm],
style=_ts(C_DARK)))
doc.build(story, canvasmaker=ProfessionalCanvas)
shutil.rmtree(tmpdir, ignore_errors=True)
print(f"report_completo.pdf generato ({len(induttori)} induttori).")
# ─── Entry point ──────────────────────────────────────────────────────────────
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ENTRY POINT
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if __name__ == "__main__":
induttori = collect_induttori()
if not induttori:
print("Nessun report*.md trovato nelle cartelle induttore*/.")
else:
generate_tex(induttori)
generate_pdf(induttori)
t_inf = read_t_inf()
generate_tex(induttori, t_inf)
generate_pdf(induttori, t_inf)
+47 -62
View File
@@ -2,120 +2,105 @@
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[margin=2.5cm]{geometry}
\usepackage{amsmath,amssymb}
\usepackage{booktabs}
\usepackage{graphicx}
\usepackage{float}
\usepackage{amsmath,amssymb,booktabs,graphicx,float}
\usepackage[table]{xcolor}
\usepackage{hyperref}
\usepackage[colorlinks,linkcolor=blue,urlcolor=blue]{hyperref}
\usepackage{fancyhdr}
\definecolor{darkblue}{HTML}{1a3a5c}
\pagestyle{fancy}\fancyhf{}
\rhead{\textcolor{darkblue}{\small Cevolani S.p.A.}}
\lhead{\textcolor{darkblue}{\small Analisi Termica Forno}}
\lhead{\textcolor{darkblue}{\small\textbf{Cevolani Italia s.r.l.}}}
\chead{\textcolor{darkblue}{\small Analisi Termica Forno}}
\rhead{\textcolor{darkblue}{\small RT-TERM-001\ --\ Rev. 00}}
\lfoot{\textcolor{gray}{\tiny USO INTERNO}}
\cfoot{\textcolor{gray}{\tiny RT-TERM-001\ |\ Rev. 00\ |\ 14/05/2026}}
\rfoot{\thepage}
\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.}
\title{\textbf{Report Analisi Termica Forno}\\[0.4em]\large Caratterizzazione termica degli induttori}
\author{Ufficio Tecnico Meccanico \\ Cevolani Italia s.r.l.}
\date{\today}
\begin{document}
\maketitle
\tableofcontents
\newpage
\maketitle\tableofcontents\newpage
\section{Modello di fitting}
Ogni intervallo è interpolato con il modello di raffreddamento di Newton:
Ogni intervallo è interpolato con la legge di raffreddamento di Newton:
\begin{equation}
T(t) = T_\infty + A \cdot e^{-(t-t_0)/\tau}
T(t) = T_{\infty} + A\,e^{-(t-t_0)/\tau}
\end{equation}
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}.
con $T_{\infty}=22{,}99\,^{\circ}\mathrm{C}$ fissa (media ponderata), parametri $A$ e $\tau$ liberi stimati con TRF (\texttt{scipy.optimize.curve\_fit}).
\section{Report Induttore 1}
\subsection{Intervallo di campionamento}
\begin{tabular}{ll}\toprule
\textbf{Parametro} & \textbf{Valore} \\\midrule
Inizio & $110.81$ s \\
Fine & $111.56$ s \\
Numero di punti & 5 \\
\textbf{Parametro}&\textbf{Valore}\\\midrule
Inizio&$110.81$ s\\
Fine&$111.56$ s\\
N.\,punti&5\\
\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{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} \\
\textbf{Param.}&\textbf{Valore}&\textbf{Incert.\,($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}
\subsection{Grafico}
\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]}
\includegraphics[width=.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
\section{Report Induttore 2}
\subsection{Intervallo di campionamento}
\begin{tabular}{ll}\toprule
\textbf{Parametro} & \textbf{Valore} \\\midrule
Inizio & $112.02$ s \\
Fine & $113.95$ s \\
Numero di punti & 12 \\
\textbf{Parametro}&\textbf{Valore}\\\midrule
Inizio&$112.02$ s\\
Fine&$113.95$ s\\
N.\,punti&12\\
\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{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} \\
\textbf{Param.}&\textbf{Valore}&\textbf{Incert.\,($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}
\subsection{Grafico}
\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]}
\includegraphics[width=.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
\section{Report Induttore 3}
\subsection{Intervallo di campionamento}
\begin{tabular}{ll}\toprule
\textbf{Parametro} & \textbf{Valore} \\\midrule
Inizio & $115.0$ s \\
Fine & $133.7$ s \\
Numero di punti & 125 \\
\textbf{Parametro}&\textbf{Valore}\\\midrule
Inizio&$115.0$ s\\
Fine&$133.7$ s\\
N.\,punti&125\\
\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{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} \\
\textbf{Param.}&\textbf{Valore}&\textbf{Incert.\,($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}
\subsection{Grafico}
\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]}
\includegraphics[width=.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
\section{Riepilogo}
\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 \\
\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}
\end{document}