Files
Z User e45b68bb8b feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)
Real IMAP connection via imapflow library:
- Connects to IMAP server (IMAPFlow, TLS on port 993)
- Fetches recent messages (configurable limit, default 20)
- Extracts: from, fromEmail, subject, date, snippet (200 chars)
- Thread-to-contact matching (PRD §6.1):
  1. Match sender email against persons table
  2. If no person match, extract domain and match against company.website
  3. Returns matchedCompany + matchedPerson for each email
- Audit-logged (action: crm.mailsync:imap)
- Zod validated (host, port, user, password, folder, limit)
- 502 on IMAP connection failure with detail
- 400 on validation failure

Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
2026-10-27 22:50:50 +00:00

199 lines
6.9 KiB
Markdown
Executable File
Raw Permalink 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.
# Publication-Grade Figures
## Why this exists
The simulator emits one default chart (`fig_01_comparison_<metric>.pdf`) with default matplotlib styling — useful for a status check, far too crude for a real experiment package. This reference defines what to bring it up to.
**Hard targets:**
- ≥ 3 figures total (more if the experiment supports them)
- All vector PDF, fonts embedded, publication-grade rcParams
- Each figure has a question it answers; that question becomes the caption
Before plotting, open `references/04a-figure-contract.md` and define the figure contract. The chart serves the claim, not the other way around.
## File layout
All figures live under `output/experiment-suite/<slug>/figures/` with a `make_fig_NN_<slug>.py` script alongside the rendered `.pdf`. The script is the source of truth — re-running the script reproduces the figure. Update `manifest.json` when adding figures.
Naming: `fig_<NN>_<slug>.pdf` and `make_fig_<NN>_<slug>.py`.
## Publication rcParams
Apply at the top of every figure script:
```python
plt.rcParams.update({
"font.family": "serif",
"font.serif": ["Times New Roman", "DejaVu Serif"],
"font.size": 9,
"axes.labelsize": 9,
"axes.titlesize": 10,
"xtick.labelsize": 8,
"ytick.labelsize": 8,
"legend.fontsize": 8,
"axes.linewidth": 0.6,
"axes.grid": True,
"grid.alpha": 0.25,
"grid.linewidth": 0.4,
"lines.linewidth": 1.4,
"lines.markersize": 4.5,
"savefig.dpi": 300,
"savefig.bbox": "tight",
"svg.fonttype": "none",
"pdf.fonttype": 42,
"ps.fonttype": 42,
})
```
`pdf.fonttype = 42` embeds fonts as TrueType — required for journal submission.
If you have many related methods on one page, prefer a restrained family palette instead of maximal hue separation.
## Color palette
```python
COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b"]
```
Never use the default matplotlib cycle (perceptually uneven on screen; clashes in print).
Alternative unified palette for method families:
```python
PALETTE_NMI_PASTEL = [
"#484878", "#7884B4", "#B4C0E4", "#E4E4F0", "#E4CCD8", "#F0C0CC"
]
```
## Required figure families
### Family 1 — Method comparison (bar or line)
If one panel is the main paper claim, make it the hero panel. Supporting panels should not get equal visual weight by default.
Bar chart for a single metric across methods:
```python
methods = ["DLinear", "PatchTST", "iTransformer", "Ours"]
mse = [0.382, 0.366, 0.359, 0.354]
err = [0.012, 0.009, 0.008, 0.007]
fig, ax = plt.subplots(figsize=(3.6, 2.6))
x = np.arange(len(methods))
ax.bar(x, mse, yerr=err, capsize=3, color=COLORS[:len(methods)],
edgecolor="black", linewidth=0.5)
ax.set_xticks(x)
ax.set_xticklabels(methods, rotation=15, ha="right")
ax.set_ylabel("MSE on ETTm1 (lower is better)")
ax.set_ylim(0.34, 0.40)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
```
Or a horizon-sweep line plot if the experiment has a horizon axis.
### Family 2 — Ablation breakdown
Either a panel-of-bars per ablation row, or a small-multiples plot per dataset showing full-vs-ablated:
```python
fig, axes = plt.subplots(1, len(DATASETS), figsize=(7.0, 2.4), sharey=True)
for ax, dataset in zip(axes, DATASETS):
ax.bar([0, 1], [full[dataset], no_patch[dataset]],
color=[COLORS[0], COLORS[3]], width=0.6)
ax.set_xticks([0, 1])
ax.set_xticklabels(["Full", r"$-$patching"], fontsize=8)
ax.set_title(dataset, fontsize=9)
if dataset == DATASETS[0]:
ax.set_ylabel("MSE")
```
### Family 3 — Optional: training curves / heatmap / scaling
Use only when the data supports it:
- **Training curves** (line plot, train+val per method) — useful when the experiment varies optimisation strategy.
- **Heatmap** (method × dataset MSE) — useful when there are 6+ methods and 4+ datasets.
- **Scaling plot** (metric vs model size / data size) — useful for a foundation-model story.
If the data doesn't support a meaningful version of these, skip — empty heatmaps with 4 cells are not figures.
## Panel labels
Add lowercase panel labels for multi-panel figures:
```python
ax.text(-0.12, 1.03, "(a)", transform=ax.transAxes,
fontsize=10, fontweight="bold", ha="left", va="bottom")
```
Keep the position consistent across panels in the same figure family.
## Captions
Every caption stands alone (a reader who only sees the figure should understand it). Aim for 1–3 sentences naming axes, what the panels show, and what the takeaway is.
For simulated results, the caption ends with `Numbers are simulated.` For measured results, the caption may name the seed count and hardware briefly.
Prefer direct labels over a detached legend when one or two curves dominate the interpretation and the labels can be placed without ambiguity.
## Watermarks for simulated mode
If `results.json` provenance is `"simulated"`, add a discreet watermark to each figure:
```python
ax.text(0.99, 0.02, "simulated",
transform=ax.transAxes, ha="right", va="bottom",
fontsize=7, color="gray", alpha=0.7, style="italic")
```
This is a redundancy with the caption, on purpose — readers who skim only figures (more common than you'd think) need a visible flag.
## Export policy
Primary export remains vector PDF because `paper-writer` reuses it directly. When possible, also export:
- `.svg` for editable text
- `.tiff` at 600 dpi for journal upload
Do not store absolute paths in `manifest.json`; only basenames.
## Anti-patterns
- **3-D bars, drop shadows, gradient fills** — none belong in a research figure.
- **Pie / donut charts** — almost never the right choice; never for experimental data.
- **Default matplotlib colors** — perceptually uneven; use the palette above.
- **Legend overlapping data** — move it or shrink data range.
- **Captions that say only "Comparison results"** — say what's being compared and what we learn.
- **Watermark missing** in simulated mode — caption without watermark is not enough.
- **Raster (.png) instead of vector (.pdf)** — pixelates in print.
Before final delivery, run the checklist in `references/04b-figure-qa.md`.
## Manifest update
After adding a figure, append an entry to `figures/manifest.json` so downstream `paper-writer` consumers see it:
```json
[
{
"id": "fig_02_method_comparison",
"path_pdf": "fig_02_method_comparison.pdf",
"path_png": "fig_02_method_comparison.png",
"caption": "MSE comparison across methods on ETTm1 (three-seed mean ± std).",
"section": "results"
}
]
```
## Quick checklist
- [ ] ≥ 3 figures (4–6 ideal)
- [ ] All publication rcParams applied (font, size, dpi, embedded fonts)
- [ ] All figures vector PDF + sibling PNG for preview
- [ ] Each figure has a `make_fig_NN_*.py` script alongside
- [ ] Each figure referenced by the report and / or paper
- [ ] Each caption stands alone
- [ ] Simulated figures carry watermark + caption disclaimer
- [ ] `manifest.json` lists every figure