Files
report-temperatura/report_completo.py
T

443 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from pathlib import Path
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.pdfgen import canvas as rl_canvas
from datetime import date
BASE = Path(__file__).parent
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
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), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
("LEFTPADDING", (0, 0), (-1, -1), 12),
])
# ─── Parser markdown ──────────────────────────────────────────────────────────
def parse_md(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
kv = dict(re.findall(r"^\| (.+?) \| (.+?) \|$", text, re.MULTILINE))
kv = {k.strip(): v.strip() for k, v in kv.items() if k.strip() != "---"}
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"
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", "∞"))
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
def collect_induttori() -> list:
return [parse_md(md)
for folder in sorted(BASE.glob("induttore*/"))
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):
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}.",
"",
]
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}"]
(BASE / "report_completo.tex").write_text("\n".join(lines), encoding="utf-8")
print("report_completo.tex generato.")
# ─── 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,
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]),
])
story = []
# ── 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))
# Titolo principale
story.append(HRFlowable(width="100%", thickness=4, color=C_BLUE))
story.append(Spacer(1, 0.4*cm))
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"]))
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(
"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))
# ── SEZIONI INDUTTORI ────────────────────────────────────────────────────────
for i, ind in enumerate(induttori, 1):
story += section_header(f"{i + 1}. {ind['nome']}", s["h1"])
# Campionamento
story.append(Paragraph("Intervallo di campionamento", s["h2"]))
rows = [["Parametro", "Valore"],
["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 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 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 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))
# 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),
Paragraph(
f"Figura {i}: fit esponenziale — {ind['nome']} "
f"[{ind['intervallo']}]", s["caption"]),
]))
story.append(Spacer(1, 0.5*cm))
# ── RIEPILOGO ────────────────────────────────────────────────────────────────
story.append(PageBreak())
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, 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"]))
doc.build(story, canvasmaker=ProfessionalCanvas)
print(f"report_completo.pdf generato ({len(induttori)} induttori).")
# ─── 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)