paperplot-quantum 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
paperplot/save.py ADDED
@@ -0,0 +1,99 @@
1
+ """Saving figures with publication-correct defaults.
2
+
3
+ PDF default, EPS first-class. Fonts embedded (Type-42, no Type-3) for pdf+ps.
4
+ Runs preflight() and surfaces warnings. Warns on alpha in EPS (matplotlib
5
+ rasterizes it) and stamps the spec revision into PDF metadata.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import warnings
12
+ from pathlib import Path
13
+
14
+ import matplotlib as mpl
15
+
16
+ # pp.style() shadows the style submodule on the package; reach it via sys.modules.
17
+ _style = importlib.import_module("paperplot.style")
18
+ from .lint import preflight as _preflight
19
+
20
+ _VECTOR = {"pdf", "eps", "svg", "ps"}
21
+
22
+
23
+ def _color_has_alpha(artist) -> bool:
24
+ """True if any face/edge color on the artist is RGBA with alpha < 1."""
25
+ for getter in ("get_facecolor", "get_edgecolor"):
26
+ fn = getattr(artist, getter, None)
27
+ if fn is None:
28
+ continue
29
+ try:
30
+ col = fn()
31
+ except Exception:
32
+ continue
33
+ import numpy as np
34
+ arr = np.atleast_2d(np.asarray(col, dtype=float))
35
+ if arr.ndim == 2 and arr.shape[1] == 4 and (arr[:, 3] < 1.0).any():
36
+ return True
37
+ return False
38
+
39
+
40
+ def _has_alpha(fig) -> bool:
41
+ # Scan figure-level artists + every axes child for transparency, via the
42
+ # alpha attribute *and* RGBA face/edge colors (fill_between, scatter, etc.).
43
+ artists = [fig.patch, *getattr(fig, "legends", [])]
44
+ for ax in fig.axes:
45
+ artists.extend(ax.get_children())
46
+ for artist in artists:
47
+ a = getattr(artist, "get_alpha", lambda: None)()
48
+ if a is not None and a < 1.0:
49
+ return True
50
+ if _color_has_alpha(artist):
51
+ return True
52
+ return False
53
+
54
+
55
+ def save(fig, path, *, dpi=None, run_preflight=True, journal=None, **savefig_kw):
56
+ """Save ``fig`` to ``path`` with journal-correct export settings.
57
+
58
+ Format is taken from the extension; no extension defaults to ``.pdf``.
59
+ Returns the preflight :class:`~paperplot.lint.Report` (or None if skipped).
60
+ """
61
+ spec = _style.resolve_spec(journal)
62
+ p = Path(path)
63
+ fmt = p.suffix.lower().lstrip(".")
64
+ if not fmt:
65
+ p = p.with_suffix(".pdf")
66
+ fmt = "pdf"
67
+
68
+ if fmt in ("eps", "ps") and _has_alpha(fig):
69
+ warnings.warn(
70
+ f"{fmt.upper()} target: figure has alpha<1 artists; matplotlib "
71
+ f"rasterizes transparency in {fmt.upper()}. Use PDF to keep them vector.",
72
+ stacklevel=2,
73
+ )
74
+
75
+ report = None
76
+ if run_preflight:
77
+ report = _preflight(fig, spec)
78
+ for f in report.warnings:
79
+ warnings.warn(f"preflight: {f.message}", stacklevel=2)
80
+
81
+ if dpi is None and fmt not in _VECTOR:
82
+ dpi = spec.rasterize_dpi.get("line", 600)
83
+
84
+ metadata = None
85
+ if fmt == "pdf":
86
+ metadata = {"Creator": f"paperplot ({spec.name} {spec.revision})"}
87
+
88
+ kw = dict(facecolor="white")
89
+ if dpi is not None:
90
+ kw["dpi"] = dpi
91
+ if metadata is not None:
92
+ kw["metadata"] = metadata
93
+ kw.update(savefig_kw)
94
+
95
+ # Embed fonts as Type-42 (never Type-3) scoped to this save — don't mutate
96
+ # the caller's global rcParams.
97
+ with mpl.rc_context({"pdf.fonttype": 42, "ps.fonttype": 42}):
98
+ fig.savefig(p, **kw)
99
+ return report
paperplot/style.py ADDED
@@ -0,0 +1,194 @@
1
+ """rcParams construction and the use/style/reset lifecycle.
2
+
3
+ ``rcparams(spec, ...)`` is the core translation layer (spec -> matplotlib rc).
4
+ ``use()`` sets a sticky global journal; ``style()`` is a scoped context manager
5
+ (full save/restore); ``reset()`` undoes ``use()``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import copy
12
+ from typing import Optional
13
+
14
+ import matplotlib as mpl
15
+ from cycler import cycler
16
+
17
+ from . import fonts, palettes
18
+ from .journals import JournalSpec
19
+ from .registry import get_spec
20
+
21
+ _active: Optional[JournalSpec] = None
22
+ _snapshot: Optional[dict] = None
23
+
24
+ # The styling options last set by use(); figure()/style() inherit these unless a
25
+ # call overrides them (pass the kwarg). reset() restores the defaults.
26
+ _DEFAULT_OPTS = {"serif": False, "palette": None, "usetex": False,
27
+ "math": "cm", "font_scale": 1.0}
28
+ _active_opts: dict = dict(_DEFAULT_OPTS)
29
+
30
+ # math= names -> matplotlib mathtext.fontset. Default "cm" gives the Computer
31
+ # Modern look physics figures expect, even with sans-serif text labels.
32
+ _MATH_FONTSET = {
33
+ "cm": "cm", # Computer Modern (LaTeX look) — default
34
+ "stix": "stix", # Times-like math, pairs with serif=True
35
+ "stixsans": "stixsans",
36
+ "sans": "stixsans", # clean sans math that pairs with Arial/Helvetica
37
+ "dejavusans": "dejavusans",
38
+ }
39
+
40
+
41
+ def _mathset(math: str, serif: bool) -> str:
42
+ if math == "auto":
43
+ return "cm" if serif else "stixsans"
44
+ return _MATH_FONTSET.get(math, math)
45
+
46
+
47
+ def _effective(serif=None, palette=None, usetex=None, math=None,
48
+ font_scale=None) -> dict:
49
+ """Fill any unspecified (None) option from the active use() options."""
50
+ o = _active_opts
51
+ return {
52
+ "serif": o["serif"] if serif is None else serif,
53
+ "palette": o["palette"] if palette is None else palette,
54
+ "usetex": o["usetex"] if usetex is None else usetex,
55
+ "math": o["math"] if math is None else math,
56
+ "font_scale": o["font_scale"] if font_scale is None else font_scale,
57
+ }
58
+
59
+
60
+ def resolve_spec(journal) -> JournalSpec:
61
+ """Resolve a journal arg (str / JournalSpec / None) to a spec."""
62
+ if isinstance(journal, JournalSpec):
63
+ return journal
64
+ if journal is not None:
65
+ return get_spec(journal)
66
+ if _active is not None:
67
+ return _active
68
+ raise RuntimeError(
69
+ "No active journal. Call pp.use('aps') first, or pass journal=..."
70
+ )
71
+
72
+
73
+ def rcparams(spec: JournalSpec, *, serif: bool = False, usetex: bool = False,
74
+ palette=None, math: str = "cm", font_scale: float = 1.0) -> dict:
75
+ """Build the matplotlib rcParams dict for a spec.
76
+
77
+ ``math`` selects the mathtext font set (``"cm"``/``"sans"``/``"stix"``/
78
+ ``"auto"``); ``font_scale`` multiplies every spec point size (a deliberate
79
+ nudge — preflight still warns below the journal's absolute minimum).
80
+ """
81
+ fonts.register_bundled()
82
+ fp = spec.font_pt
83
+ s = float(font_scale)
84
+ lw = spec.min_linewidth_pt
85
+ cycle_colors = palettes.resolve_cycle(palette)
86
+ return {
87
+ # fonts
88
+ "font.family": "serif" if serif else "sans-serif",
89
+ "font.sans-serif": list(spec.font_family),
90
+ "font.serif": ["Times", "Nimbus Roman", "DejaVu Serif"],
91
+ "font.size": fp.base * s,
92
+ "axes.labelsize": fp.base * s,
93
+ "axes.titlesize": fp.base * s,
94
+ "xtick.labelsize": fp.tick * s,
95
+ "ytick.labelsize": fp.tick * s,
96
+ "legend.fontsize": fp.legend * s,
97
+ "legend.title_fontsize": fp.base * s,
98
+ "figure.titlesize": fp.base * s,
99
+ "mathtext.fontset": _mathset(math, serif),
100
+ "text.usetex": usetex,
101
+ "axes.unicode_minus": not usetex,
102
+ "axes.formatter.use_mathtext": True,
103
+ # lines / ticks / spines (>= min line weight). Plot lines scale with the
104
+ # journal's min weight so talk targets get thicker strokes for projection
105
+ # while APS/Nature (min 0.5) keep the 1.0 pt publication default.
106
+ "axes.linewidth": max(0.6, lw),
107
+ "lines.linewidth": max(1.0, 2.0 * lw),
108
+ "lines.markersize": max(3.0, 3.0 * lw / 0.5),
109
+ "lines.markeredgewidth": 0.5,
110
+ "patch.linewidth": 0.6,
111
+ "xtick.direction": "in",
112
+ "ytick.direction": "in",
113
+ "xtick.major.width": lw,
114
+ "ytick.major.width": lw,
115
+ "xtick.minor.width": 0.4,
116
+ "ytick.minor.width": 0.4,
117
+ "xtick.major.size": 3.0,
118
+ "ytick.major.size": 3.0,
119
+ "xtick.minor.size": 1.5,
120
+ "ytick.minor.size": 1.5,
121
+ "xtick.major.pad": 3,
122
+ "ytick.major.pad": 3,
123
+ "axes.titlepad": 3,
124
+ "axes.labelpad": 2,
125
+ "axes.xmargin": 0.02,
126
+ "axes.ymargin": 0.02,
127
+ # full box (APS); pp.despine() removes top/right on demand
128
+ "axes.spines.top": True,
129
+ "axes.spines.right": True,
130
+ # color cycle
131
+ "axes.prop_cycle": cycler(color=cycle_colors),
132
+ # figure / save
133
+ "figure.figsize": list(spec.figsize("single")),
134
+ "figure.facecolor": "white",
135
+ "figure.dpi": 150,
136
+ "savefig.dpi": spec.rasterize_dpi.get("line", 600),
137
+ "savefig.bbox": "standard",
138
+ "savefig.pad_inches": 0.01,
139
+ "savefig.transparent": False,
140
+ "pdf.fonttype": 42,
141
+ "ps.fonttype": 42,
142
+ }
143
+
144
+
145
+ def apply(spec: JournalSpec, *, serif=None, palette=None, usetex=None,
146
+ math=None, font_scale=None) -> None:
147
+ """Update the global rcParams in place (used by use() and figure()).
148
+
149
+ Unspecified (None) options fall back to whatever ``use()`` last set, so
150
+ per-figure calls inherit the active style instead of resetting it.
151
+ """
152
+ eff = _effective(serif, palette, usetex, math, font_scale)
153
+ mpl.rcParams.update(rcparams(spec, **eff))
154
+
155
+
156
+ def use(journal, *, serif: bool = False, palette=None, usetex: bool = False,
157
+ math: str = "cm", font_scale: float = 1.0) -> JournalSpec:
158
+ """Set the sticky active journal + style options and apply them globally."""
159
+ global _active, _snapshot, _active_opts
160
+ spec = resolve_spec(journal)
161
+ if _snapshot is None: # remember the pre-paperplot state for reset()
162
+ _snapshot = copy.deepcopy(dict(mpl.rcParams))
163
+ _active_opts = {"serif": serif, "palette": palette, "usetex": usetex,
164
+ "math": math, "font_scale": font_scale}
165
+ apply(spec) # uses the just-stored active options
166
+ _active = spec
167
+ return spec
168
+
169
+
170
+ def active() -> Optional[JournalSpec]:
171
+ return _active
172
+
173
+
174
+ def reset() -> None:
175
+ """Restore rcParams to the state before the first use(), and clear active."""
176
+ global _active, _snapshot, _active_opts
177
+ if _snapshot is not None:
178
+ mpl.rcParams.update(_snapshot)
179
+ _snapshot = None
180
+ _active = None
181
+ _active_opts = dict(_DEFAULT_OPTS)
182
+
183
+
184
+ @contextlib.contextmanager
185
+ def style(journal=None, *, serif=None, palette=None, usetex=None,
186
+ math=None, font_scale=None):
187
+ """Scoped styling: applies a spec's rcParams, restores fully on exit.
188
+
189
+ Unspecified options inherit the active ``use()`` style (or the defaults).
190
+ """
191
+ spec = resolve_spec(journal)
192
+ eff = _effective(serif, palette, usetex, math, font_scale)
193
+ with mpl.rc_context(rcparams(spec, **eff)):
194
+ yield spec
@@ -0,0 +1,281 @@
1
+ Metadata-Version: 2.4
2
+ Name: paperplot-quantum
3
+ Version: 0.1.0
4
+ Summary: Publication-correct matplotlib figures, journal by journal (APS, Nature, IEEE, + presentation).
5
+ Author: Zlatko Minev
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/zlatko-minev/paperplot
8
+ Project-URL: Documentation, https://zlatko-minev.github.io/paperplot/
9
+ Project-URL: Repository, https://github.com/zlatko-minev/paperplot
10
+ Project-URL: Issues, https://github.com/zlatko-minev/paperplot/issues
11
+ Keywords: matplotlib,publication,figures,physics,APS,Physical Review
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Visualization
22
+ Classifier: Framework :: Matplotlib
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ License-File: paperplot/data/fonts/LICENSE.md
27
+ License-File: paperplot/data/fonts/GUST-FONT-LICENSE.txt
28
+ Requires-Dist: matplotlib>=3.6
29
+ Requires-Dist: numpy>=1.21
30
+ Requires-Dist: tomli>=1.2; python_version < "3.11"
31
+ Provides-Extra: notebook
32
+ Requires-Dist: IPython>=7; extra == "notebook"
33
+ Provides-Extra: seaborn
34
+ Requires-Dist: seaborn>=0.12; extra == "seaborn"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7; extra == "dev"
37
+ Provides-Extra: docs
38
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
39
+ Requires-Dist: mkdocs-jupyter>=0.24; extra == "docs"
40
+ Requires-Dist: jupytext>=1.16; extra == "docs"
41
+ Requires-Dist: nbconvert>=7; extra == "docs"
42
+ Requires-Dist: ipykernel>=6; extra == "docs"
43
+ Dynamic: license-file
44
+
45
+ # paperplot
46
+
47
+ [![CI](https://github.com/zlatko-minev/paperplot/actions/workflows/ci.yml/badge.svg)](https://github.com/zlatko-minev/paperplot/actions/workflows/ci.yml)
48
+ [![Docs](https://github.com/zlatko-minev/paperplot/actions/workflows/docs.yml/badge.svg)](https://zlatko-minev.github.io/paperplot/)
49
+ [![Python](https://img.shields.io/badge/python-%E2%89%A53.10-blue)](https://www.python.org/)
50
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
51
+ [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zlatko-minev/paperplot/blob/main/docs/showcase.ipynb)
52
+ [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/zlatko-minev/paperplot/main?labpath=docs/showcase.ipynb)
53
+
54
+ **Publication-correct matplotlib figures, journal by journal.**
55
+
56
+ Most matplotlib styling focuses on the *look*. `paperplot` does that too — but it
57
+ also handles the parts that decide whether a journal accepts your figure:
58
+
59
+ - **sizes it to the journal's real column width** (8.6 cm, 17.8 cm, …), not a generic default;
60
+ - **embeds fonts correctly** (Type-42, no Type-3);
61
+ - **preflights it** for the rules figures actually get rejected over — font type,
62
+ line weight, lettering height, and grayscale legibility.
63
+
64
+ Ships **Physical Review (APS)** (incl. PRL/PRX/PRB), **Nature**, and **IEEE**, plus a
65
+ **presentation** target for slides. matplotlib-only core; seaborn/IPython are optional
66
+ extras.
67
+
68
+ > **Just want the look?** `pp.register_mplstyles()` then `plt.style.use("paperplot-aps")` —
69
+ > no API to learn. You give up only the parts a style sheet *can't* do (true column
70
+ > sizing, font embedding, preflight); `pp.figure()`/`pp.save()` add those back. See
71
+ > [Drop-in style sheets](#drop-in-style-sheets).
72
+
73
+ ## Install
74
+
75
+ ```bash
76
+ pip install paperplot-quantum # core (matplotlib + numpy)
77
+ pip install "paperplot-quantum[notebook]" # + IPython for pp.show()
78
+ ```
79
+
80
+ The PyPI package is **`paperplot-quantum`**, but you still **`import paperplot`**.
81
+ Python ≥ 3.10. Latest `main` from GitHub:
82
+ `pip install "git+https://github.com/zlatko-minev/paperplot.git"`. Full matrix on
83
+ the [Install page](https://zlatko-minev.github.io/paperplot/install/).
84
+
85
+ ## 30-second quickstart
86
+
87
+ ```python
88
+ import numpy as np
89
+ import paperplot as pp
90
+
91
+ pp.use("aps") # or "nature" / "ieee" / "prl" / "talk" — set once
92
+
93
+ fig, ax = pp.figure(width="single") # 8.6 cm wide, golden ratio, styled
94
+ ax.plot(np.linspace(0, 10, 200), np.sin(np.linspace(0, 10, 200)))
95
+ ax.set_xlabel(r"delay $\tau$ (ns)")
96
+ ax.set_ylabel(r"population $\langle n \rangle$")
97
+
98
+ pp.save(fig, "fig1.pdf") # embeds fonts, runs preflight()
99
+ ```
100
+
101
+ ## Gallery
102
+
103
+ Same three-curve plot, same data — stock matplotlib versus one `pp.use("aps")`.
104
+ paperplot fixes the column width, type scale, color cycle, ticks, and line weights
105
+ in a single call.
106
+
107
+ <table>
108
+ <tr>
109
+ <td align="center"><b>matplotlib defaults</b><br><img src="docs/assets/gallery/before.png" width="380"></td>
110
+ <td align="center"><b>paperplot — <code>pp.use("aps")</code></b><br><img src="docs/assets/gallery/after.png" width="380"></td>
111
+ </tr>
112
+ </table>
113
+
114
+ <table>
115
+ <tr>
116
+ <td><img src="docs/assets/gallery/aps_4panel_single.png" width="250"></td>
117
+ <td><img src="docs/assets/gallery/hist_overlap.png" width="250"></td>
118
+ <td><img src="docs/assets/gallery/data_fit_band.png" width="250"></td>
119
+ </tr>
120
+ </table>
121
+
122
+ ### One figure, many panels
123
+
124
+ A custom `GridSpec`, four panel letters: **(a)** $T_1$ relaxation and **(b)** Ramsey
125
+ fringes — each a fit with a **flush residual strip** — plus **(c)** outlined $T_1$
126
+ distributions and **(d)** a chevron map. Journal styling carries through a hand-built grid.
127
+
128
+ <p align="center"><img src="docs/assets/gallery/composite.png" width="720"></p>
129
+
130
+ ### See it on the page before you submit
131
+
132
+ The part most styling packages skip: `pp.preview_in_page(fig)` drops your figure
133
+ into a **true-to-scale mock journal page** with real body text — so you see exactly
134
+ how big it lands in the column and whether the lettering still reads, *before* you
135
+ submit. More in the [gallery](https://zlatko-minev.github.io/paperplot/gallery/#on-the-page).
136
+
137
+ <table>
138
+ <tr>
139
+ <td align="center"><img src="docs/assets/gallery/in_page_aps.png" width="250"><br>single column</td>
140
+ <td align="center"><img src="docs/assets/gallery/in_page_hist.png" width="250"><br>histograms, in context</td>
141
+ <td align="center"><img src="docs/assets/gallery/in_page_double.png" width="250"><br>double column, across the page</td>
142
+ </tr>
143
+ </table>
144
+
145
+ Every image is generated from [`examples/showcase.py`](examples/showcase.py) by
146
+ [`docs/generate_gallery.py`](docs/generate_gallery.py) and regenerated by CI on each
147
+ docs build (published to GitHub Pages). Build locally:
148
+
149
+ ```bash
150
+ pip install -e ".[docs,notebook]"
151
+ python docs/generate_gallery.py && mkdocs serve
152
+ ```
153
+
154
+ ## What you get
155
+
156
+ | | |
157
+ |---|---|
158
+ | **True journal sizing** | `width="single"/"onehalf"/"double"/"full_page"` → exact column widths in inches. |
159
+ | **Correct export** | PDF default, EPS first-class; fonts embedded as Type-42, no Type-3; RGB; revision stamped in metadata. |
160
+ | **Preflight linter** | `pp.preflight(fig)` → structured `Report`: flags sub-spec fonts/lines and grayscale-ambiguous colors. Warn, never block. |
161
+ | **Colorblind-safe by default** | Okabe-Ito cycle; sequential/diverging maps kept separate (`pp.cmap("BuGn")`); custom via `pp.register_palette`. Fill/stroke convention: `pp.fills()` (muted) under `pp.strokes()` (bright). `pp.show_palettes()` shows every palette tagged for colorblind-safety. |
162
+ | **Plot helpers** | `pp.hist_outline` (translucent fill + crisp staircase outline), `pp.hist_filled`, `pp.data_fit_band` (markers + fit + CI band), `pp.swatches`. |
163
+ | **See it on the page** | `pp.preview_in_page(fig)` drops the figure into a true-to-scale mock journal page with real body text — catch sizing/legibility before you submit (a paperplot original). Plus `pp.show(fig, zoom=2)` and `pp.grayscale_proof(fig)`. |
164
+ | **Helpers** | `pp.panel_labels(axes)`, `pp.despine(ax)`, `pp.clean_shared_axes(fig)`. |
165
+
166
+ ## A bit more
167
+
168
+ ```python
169
+ # multi-panel, double column
170
+ fig, axes = pp.figure(width="double", nrows=2, ncols=2, sharex=True)
171
+ pp.panel_labels(axes, fmt="({})") # (a) (b) (c) (d)
172
+ pp.clean_shared_axes(fig)
173
+
174
+ # sans-serif labels with Computer Modern math is the default (the physics look);
175
+ # math="sans" pairs Arial-style math instead, font_scale nudges every size
176
+ pp.use("aps", math="cm", font_scale=1.1) # sticky: pp.figure() inherits both
177
+
178
+ # scoped style (doesn't leak), serif to match REVTeX Times
179
+ with pp.style("aps", serif=True):
180
+ fig, ax = pp.figure(width="full_page")
181
+
182
+ # per-figure overrides
183
+ fig, ax = pp.figure("single", journal="prl", palette="mylab")
184
+
185
+ # overlapping histograms, fig-3 style: muted fills, crisp outlines
186
+ fig, ax = pp.figure("single")
187
+ for s, c in zip((sample_a, sample_b), pp.fills()):
188
+ pp.hist_outline(s, ax, bins=60, rescale=True, color=c)
189
+ ax.axvline(ideal, color=pp.strokes()[8], ls="--", lw=1.2, zorder=100)
190
+
191
+ # data + fit + ±1σ band (e.g. lmfit result.eval / eval_uncertainty)
192
+ pp.data_fit_band(ax, x, y, yerr=yerr, x_fit=xf, y_fit=yf, y_fit_err=yf_err)
193
+
194
+ # pre-submission check
195
+ report = pp.preflight(fig)
196
+ if not report: # truthy == clean
197
+ print(report) # aligned table of issues
198
+ ```
199
+
200
+ ## Targets
201
+
202
+ `pp.use(...)` / `pp.figure(journal=...)` accept any of:
203
+
204
+ | key | target | single / double width |
205
+ |---|---|---|
206
+ | `aps` (+ `prl` `prx` `prb`) | Physical Review | 8.6 cm / 17.8 cm |
207
+ | `nature` | Nature | 8.9 cm / 18.3 cm |
208
+ | `ieee` | IEEE Transactions / conference | 8.9 cm (3.5″) / 18.2 cm (7.16″) |
209
+ | `talk` | slides / presentation | larger type, thicker lines (scale with the target) |
210
+
211
+ `pp.available()` lists them all. New journals are pure data — add a table to
212
+ [`data/journals.toml`](paperplot/data/journals.toml); no code.
213
+
214
+ ## Drop-in style sheets
215
+
216
+ The product is `pp.figure()`/`pp.save()` — they size to the real column, embed
217
+ fonts, and preflight, which a style sheet *cannot*. But the **look** is just
218
+ rcParams, and rcParams travel as `.mplstyle` files, so you can adopt it with zero
219
+ buy-in and graduate later:
220
+
221
+ ```python
222
+ import paperplot as pp, matplotlib.pyplot as plt
223
+
224
+ pp.register_mplstyles() # registers paperplot-aps/-nature/-ieee/-talk
225
+ plt.style.use("paperplot-aps") # now use plain matplotlib
226
+ # composable, matplotlib-native: layer a built-in modifier on top
227
+ plt.style.use(["paperplot-nature", "ggplot"])
228
+
229
+ # or write a file to commit / share / drop into ~/.config/matplotlib/stylelib/
230
+ pp.export_mplstyle("ieee", "ieee.mplstyle", serif=True)
231
+ ```
232
+
233
+ paperplot never touches matplotlib's global style library on import — registration
234
+ is an explicit opt-in. A style sheet carries the target's *default* (single-column)
235
+ size; reach for `pp.figure(width=...)` when you need the column-true figure.
236
+
237
+ ## Fonts
238
+
239
+ Journals want figure lettering in **Helvetica or Arial** (Nature requires it), but
240
+ those are proprietary and can't be redistributed. So paperplot ships **TeX Gyre
241
+ Heros** — a free, metric-compatible Helvetica clone — and registers it
242
+ automatically. The font preference is `Arial → Helvetica → TeX Gyre Heros →
243
+ DejaVu Sans`: if you have the real thing installed it's used, otherwise the
244
+ bundled clone gives the *same Helvetica look on every machine* (and embeds a
245
+ proper Type-42 font in the PDF, instead of silently falling back to matplotlib's
246
+ DejaVu Sans). This is why the figures look identical on your laptop, a
247
+ collaborator's box, and CI. Bundled under the free GUST Font License
248
+ ([`paperplot/data/fonts/LICENSE.md`](paperplot/data/fonts/LICENSE.md)).
249
+
250
+ ## Design
251
+
252
+ See [`paperplot_DESIGN.md`](paperplot_DESIGN.md) for the full design — journal specs
253
+ as data (`data/journals.toml`), the rcParams mapping, and the registry/versioning
254
+ model.
255
+
256
+ ## Layout
257
+
258
+ ```
259
+ paperplot/
260
+ journals.py registry.py data/journals.toml layout.py
261
+ style.py lint.py palettes.py preview.py
262
+ fonts.py save.py core.py plots.py
263
+ mplstyle.py
264
+ ```
265
+
266
+ ## Develop
267
+
268
+ ```bash
269
+ pip install -e ".[dev]"
270
+ pytest # tests
271
+ python examples/run_all.py # run every example -> examples/out/render/*.png
272
+ ```
273
+
274
+ ## Releasing
275
+
276
+ Published to PyPI as **`paperplot-quantum`** via **Trusted Publishing** (OIDC — no
277
+ stored tokens). To cut a release: bump `version` in `pyproject.toml`, push, then
278
+ create a GitHub **Release** with tag `vX.Y.Z`. The
279
+ [`release.yml`](.github/workflows/release.yml) workflow builds, runs `twine check`,
280
+ and publishes. (One-time: register the trusted publisher on PyPI — repo
281
+ `zlatko-minev/paperplot`, workflow `release.yml`, environment `pypi`.)
@@ -0,0 +1,27 @@
1
+ paperplot/__init__.py,sha256=g6mN9_2FB48JVAt90sFm0_F1nMP8enEI0ggkdN9Bfv4,2117
2
+ paperplot/core.py,sha256=Br6v9f0n1JgJL3fLKygAeVoKMQsFbVgSxq8wu1zE2FE,8778
3
+ paperplot/fonts.py,sha256=iusWnMj8Mw_lYigF7tR33vBpj2XrR90053z-WCT0RMg,1406
4
+ paperplot/journals.py,sha256=dip0ve29t_w4i9DI3ZC6TpuyFZiJKi8hg78xkAfgdHw,2019
5
+ paperplot/layout.py,sha256=EInObtV5sujqTwBJrMhIzHInvzWpMbo7aLcW_IYo_To,4205
6
+ paperplot/lint.py,sha256=_7Tz_YMTCgzIAhXAAwk42MvPiQLwKy-R1kICOODBxZA,6191
7
+ paperplot/mplstyle.py,sha256=JOdpiqen0eTwaLK4ZG9LIYWX2YzcneVsW7ngkv2UPkc,4330
8
+ paperplot/palettes.py,sha256=Baku2fIvCvJPV8NR01P8h8bpjG3RSI_qdtTHHyrT0iQ,7126
9
+ paperplot/plots.py,sha256=8_y24FVFGzENiaaENdsRjSrTeZo-2c2Xgel24ohozpk,10515
10
+ paperplot/preview.py,sha256=pXg22V9RpqcVo_fvVU9RIgr752GfiC2mrerbVN5Pi-Y,10082
11
+ paperplot/registry.py,sha256=pnuoxDubGe4SGyNDYMtMh5p7VdKrrOLLkNvr413GYVs,3696
12
+ paperplot/save.py,sha256=mBcYIDTiubR3xU7Q-P2zSgDr7Vodt2L1m0siK01jPUk,3241
13
+ paperplot/style.py,sha256=TaWtFQfVR7QMm3oTgFjjTmif4JzAx_UtzYAxYbPJv0w,7217
14
+ paperplot/data/journals.toml,sha256=94wgnEz-j2r9CpJO34midxyWFo-sJCWNxwT1RplJRWw,3802
15
+ paperplot/data/fonts/GUST-FONT-LICENSE.txt,sha256=XrYbuDa7GEXvZocXyxWzgumXdIziYp5DiMxeTD-k5DM,1417
16
+ paperplot/data/fonts/LICENSE.md,sha256=y5G2z6rQEdok7kObwDETdRtExyQD0G5fd-ldtujuMu8,1022
17
+ paperplot/data/fonts/texgyreheros-bold.otf,sha256=sXAWKDX078KIiG3UIxQG3Efhm2FM9EFoNmNVmdRKfWA,135204
18
+ paperplot/data/fonts/texgyreheros-bolditalic.otf,sha256=Fm_G0GjZyZdCgVVcs9cwNlU3qbZ2qyabtRY_WnVJZQU,135264
19
+ paperplot/data/fonts/texgyreheros-italic.otf,sha256=ZHPff6EHs_tL44lzcQr-IrBkDCrAdtUzfPEmvtmqEIw,139208
20
+ paperplot/data/fonts/texgyreheros-regular.otf,sha256=auGgnVqUA2e3qqqR7ovYosMzv-GT5wluI_kxNX1iCB8,133600
21
+ paperplot_quantum-0.1.0.dist-info/licenses/LICENSE,sha256=2bWBItGck5o3t8rMicBPLU6OKklTEG_Af-i3oMiI9SU,1078
22
+ paperplot_quantum-0.1.0.dist-info/licenses/paperplot/data/fonts/GUST-FONT-LICENSE.txt,sha256=XrYbuDa7GEXvZocXyxWzgumXdIziYp5DiMxeTD-k5DM,1417
23
+ paperplot_quantum-0.1.0.dist-info/licenses/paperplot/data/fonts/LICENSE.md,sha256=y5G2z6rQEdok7kObwDETdRtExyQD0G5fd-ldtujuMu8,1022
24
+ paperplot_quantum-0.1.0.dist-info/METADATA,sha256=-SeSEWfyy02WCcaVJke6BGdp4Yu9IGMDj59MBoQ6PkA,12636
25
+ paperplot_quantum-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
26
+ paperplot_quantum-0.1.0.dist-info/top_level.txt,sha256=wdNpTgIMXQh8QkN3Cd_oXGMKMYK2ye3vWcowAMfWrCk,10
27
+ paperplot_quantum-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The paperplot authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,29 @@
1
+ % This is version 1.0, dated 22 June 2009, of the GUST Font License.
2
+ % (GUST is the Polish TeX Users Group, https://www.gust.org.pl)
3
+ %
4
+ % For the most recent version of this license see
5
+ % https://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
6
+ % or
7
+ % https://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
8
+ %
9
+ % This work may be distributed and/or modified under the conditions
10
+ % of the LaTeX Project Public License, either version 1.3c of this
11
+ % license or (at your option) any later version.
12
+ %
13
+ % Please also observe the following clause:
14
+ % 1) it is requested, but not legally required, that derived works be
15
+ % distributed only after changing the names of the fonts comprising this
16
+ % work and given in an accompanying "manifest", and that the
17
+ % files comprising the Work, as listed in the manifest, also be given
18
+ % new names. Any exceptions to this request are also given in the
19
+ % manifest.
20
+ %
21
+ % We recommend the manifest be given in a separate file named
22
+ % MANIFEST-<fontid>.txt, where <fontid> is some unique identification
23
+ % of the font family. If a separate "readme" file accompanies the Work,
24
+ % we recommend a name of the form README-<fontid>.txt.
25
+ %
26
+ % The latest version of the LaTeX Project Public License is in
27
+ % https://www.latex-project.org/lppl.txt and version 1.3c or later
28
+ % is part of all distributions of LaTeX version 2006/05/20 or later.
29
+