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:
1 parent
bccec166e4
commit
6e5e08d82f
2 files changed
+499
-382
No files matched your search
+452
-320
@@ -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
@@ -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}
|
||||
Reference in new issue
Block a user