콘텐츠로 이동

Python reference

The functions an analysis script or figure spec calls, by task. Everything else -- building, checking, promoting -- is a command (Command reference); renderer parameters are in the Config reference and a picture of every plot in the gallery.

A new project's script (fa new project) already uses the common ones:

"""my-project: analysis and figure 1.

Created by `fa new project` on <date>. Load the input bundles (read only),
compute what the panels show, and draw them with fig_atlas renderers; every
visual setting comes from the project config (configs/config_my-project.yaml). Record any number a
legend quotes with claims.record() and register the plotted tables with
source_data.add() (AGENTS.md 4.5).
"""
from __future__ import annotations

import matplotlib  # noqa: E402

matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402

from fig_atlas import claims, get_config, save_page_figure, source_data  # noqa: E402
from fig_atlas.bundle_figure import draw_plots  # noqa: E402
from fig_atlas.renderers import place_panel_tags  # noqa: E402

CONFIG = get_config()
CFG = CONFIG.settings
RAW = CONFIG.raw_root
OUT = CONFIG.figures_root


def load(name: str, block: dict):
    """Data for a plots.<name> block that has no `bundle` (read RAW, never modify it).
    Blocks with a bundle are loaded from it automatically."""
    raise NotImplementedError(f"load the data for plots.{name}")


def main() -> None:
    fcfg = CFG.get("figures", {})
    plt.rcParams.update({"font.family": fcfg.get("font_family", "DejaVu Sans")})   # AGENTS.md 1.4
    width_mm = fcfg.get("width_mm")                               # None: the page content width
    fig_w, fig_h = CONFIG.page.figure_size_in(
        height_mm=float(fcfg.get("height_mm", 80)),
        width_frac=float(width_mm) / CONFIG.page.content_width_mm if width_mm else 1.0)
    fig = plt.figure(figsize=(fig_w, fig_h), dpi=CONFIG.dpi)
    # Every plots.<name> block in the config is one panel (fig_atlas.bundle_figure).
    columns = draw_plots(fig, CFG.get("plots", {}), CONFIG.project_root, fcfg, load=load)
    # claims.record("n_samples", n, source="...")               # numbers quoted in the legend
    # source_data.add(table, panel="a", title="...")             # values a panel plots

    tcfg = fcfg.get("tags", {})
    place_panel_tags(fig, columns, float(tcfg.get("size", 9.0)), pad_pt=float(tcfg.get("pad_pt", 2.0)))
    OUT.mkdir(parents=True, exist_ok=True)
    for fmt in CONFIG.formats:
        save_page_figure(fig, OUT / f"figure1_main.{fmt}")
    plt.close(fig)


if __name__ == "__main__":
    main()

Contents

Config and paths

Every script starts here: get_config() finds the project's config (the build passes it as CONFIG_PATH) and merges its style presets.

get_config

from fig_atlas import get_config

get_config(config_path=None)

Find and load config.yaml (or CONFIG_PATH), walking up from CWD if not given.

ProjectConfig

from fig_atlas import ProjectConfig

ProjectConfig(config_path)

Loads config.yaml and exposes typed accessors.

Environment variables override path values: set RAW_ROOT or OUTPUT_ROOT to redirect data without editing config.yaml.

A config may reference reusable named style presets via project.style (a string or a list). Each preset is a styles/<name>.yaml file whose figures/plots blocks are deep-merged underneath this config, so the config always wins and shared styling accumulates as a named asset.

  • ProjectConfig.settings -- The whole config as a dict, merged with its style presets: the figures, plots, paths and any other blocks a script reads (CONFIG.settings["plots"]["volcano"]).
  • ProjectConfig.page -- Final-print page geometry (fig_atlas.page.PageSpec) from a page: block (usually supplied by a styles/page_*.yaml preset), or None for configs not yet migrated to final-size rendering. With figures.page_columns (1, 1.5, 2, ...) the page is narrowed to that many of its named columns (page.columns_mm), so the figure is drawn at the journal's column width.
  • ProjectConfig.raw_root -- Absolute path to the raw-data root. Order: RAW_ROOT env var > paths.raw_root in config. Relative values are anchored at the project root, so callers can use this directly without a further project_root / join.
  • ProjectConfig.figures_root -- Where the script saves its figures: paths.figures_root, else outputs/<domain>/<category>/<project>/figures.
  • ProjectConfig.formats -- figures.formats (default png, pdf, svg): the files to save.
  • ProjectConfig.dpi -- figures.dpi (default 300): resolution of the PNG files.
  • ProjectConfig.project_root -- Repo root: the parent of whichever ancestor directory is named configs. Config files may live directly under configs/ (flat layout) or nested arbitrarily deep (e.g. configs/lab/paper/) under a namespace subfolder; either way, the ancestor literally named configs marks the boundary.
  • ProjectConfig.output_root -- Root for all generated, rebuildable outputs (git-ignored). Order: OUTPUT_ROOT env var > paths.output_root in config > "outputs". An unset ${OUTPUT_ROOT} placeholder in config falls back to "outputs".
  • ProjectConfig.project_name -- project.name: the project identifier.
  • ProjectConfig.require_path(path, label) -- Raise FileNotFoundError naming label if path is missing.

repo_root

from fig_atlas import repo_root

repo_root(start=None)

Locate the project repository by walking up from start (default: this file) to the first ancestor with a .fig-atlas marker (fa init) -- or, in the fig-atlas source tree and older repositories, a fig_atlas/ directory.

Depth-independent alternative to hardcoding Path(__file__).resolve().parents[N], which silently breaks if a script's own directory nesting ever changes (e.g. a project moving into a domain/category namespace subfolder under analysis/).

load_bundle_params

from fig_atlas import load_bundle_params

load_bundle_params(bundle_path)

Read plot_params from a bundle's metadata/metadata.yaml.

Returns an empty dict if the metadata file does not exist so callers can merge it unconditionally into their parameter chain.

text_param

from fig_atlas import text_param

text_param(params, key, default=None)

Look up a text/label param, distinguishing "not set" from "set empty".

params.get(key) or default is the tempting one-liner but gets this wrong: "" is falsy in Python, so an explicit empty-string override in config (meant to hide a label/title) silently falls back to default instead of being respected. Only an absent key or an explicit YAML null should fall back; "" should render as nothing.

Final size

Figures are drawn at the size they print, so point sizes in the config are the printed sizes (design decision 0001).

PageSpec

from fig_atlas import PageSpec

PageSpec(name, content_width_mm, content_height_mm, columns_mm=())

The printed area a figure is drawn at 1:1 (mm), from the config's page: block (ProjectConfig.page). figure_size_in gives the figure size in inches for plt.figure(figsize=...).

  • PageSpec.figure_size_in(height_mm=None, height_frac=None, width_frac=1.0) -- (width, height) in inches for a figure drawn on this page. Width defaults to the full content width. Height comes from an absolute height_mm or a height_frac of the content height (default: the full content height). A height taller than the page is rejected rather than silently allowed: such a figure can only be printed by shrinking it (breaking the 1:1 point sizes) or by splitting it across pages, and that choice belongs to the figure, not here.
  • PageSpec.content_width_in -- Width of the printed area, inches.
  • PageSpec.content_height_in -- Height of the printed area, inches.

save_page_figure

from fig_atlas import save_page_figure

save_page_figure(fig, path, pad_mm=0.5, **kwargs)

Save fig at exactly its canvas width, trimmed only vertically.

bbox_inches="tight" crops each figure to its own drawn extent, so figures drawn on the same page preset come out a few millimetres wider or narrower than one another (and than the page's content width). This keeps the full canvas width -- every figure on a page preset saves at the same width, ready to place 1:1 in the page's content box -- and trims only the empty space above and below the content (plus pad_mm).

Anything drawn outside the canvas horizontally would be cut off, so that is reported as a warning (the print audit flags it too).

audit_figure

from fig_atlas import audit_figure

audit_figure(fig, page, min_font_pt=5.0, min_line_pt=0.25, max_offenders=8)

How fig prints on page: drawn, shrunk to the page width if wider, and measured. Returns a dict with scale_to_page_width, pages_needed, the smallest printed text and line (min_font_printed_pt, min_line_printed_pt), the texts below min_font_pt (smallest_texts) and the verdicts font_ok, line_ok and fits_one_page. fa audit-print and preflight run it on every figure.

Drawing panels

Draw by plot name (fa renderers lists them; the gallery shows each), or let draw_plots draw every plots: block of the config.

render

from fig_atlas import render

render(name, *args, **kwargs)

Draw a plot by name -- fig_atlas.render("volcano", ax, df, params) -- wherever its renderer lives: the core, an installed domain pack, or a recipe (fig_atlas.renderer_registry.find). Scripts need not know which; fa renderers lists the names.

draw_plots

from fig_atlas.bundle_figure import draw_plots

draw_plots(fig, plots, root, layout=None, load=None)

Draw every plots: block that names a renderer; returns the place_panel_tags columns ([(letter, axes), ...] per grid column).

plots is the config's plots: mapping, root the repository (CONFIG.project_root) and layout the figures: block (grid columns, gaps). A block with a bundle is drawn from that bundle's table; for one without, load(name, block) returns the data.

draw_bundle

from fig_atlas.bundle_figure import draw_bundle

draw_bundle(bundle, width_mm=85.0, height_mm=62.0, root=None)

One bundle drawn on its own, as fa ingest bundle would draw it: its plot_type's renderer with its plot_params, on a figure of the given printed size (single column by default). Returns the figure. Used by the example gallery (docs/reference/gallery.md) and its test.

render_image_panel

from fig_atlas.renderers import render_image_panel

render_image_panel(ax, source, params)

Draw a microscope image, blot or gel onto ax from its original file.

source is one image file, or a list of greyscale files (one per channel) that are tinted and combined. params (the panel's plots: block; keys in the config reference under image_panel) sets the crop, the linear display range per channel, gamma and the scale bar (from pixel_size). Never pass a brightened or cropped copy: the adjustments are made here and recorded with the source file hashes, so the figure's README lists them and verify fails if a source image changes (AGENTS.md 4.7). Returns the recorded entry.

place_panel_tags

from fig_atlas.renderers import place_panel_tags

place_panel_tags(fig, columns, label_size, pad_pt=2.0, fontweight='bold', align_rows=None)

Place panel letters on each panel's measured outer bounding box.

columns is a list of layout columns; each column is a list of (letter, [axes, ...]) for the panels stacked in it. Each tag's left edge sits on the column's shared plumb line -- the leftmost outer edge (tick labels, axis titles, legends included) of any panel in that column -- and its baseline sits pad_pt above its own panel's outer top edge (AGENTS.md rule 1.5: tags follow the outer bounding box, not the plot spine, and stacked panels share one vertical line).

align_rows (optional) lists groups of letters that sit in one layout row, e.g. [["a", "b"], ["c", "d", "e"]]; each group's tags share the highest of their baselines so a row's tags line up horizontally even when the panels' titles differ in height.

Call after everything in the panels has been drawn; the layout is measured with a draw pass.

panel_label

from fig_atlas.renderers import panel_label

panel_label(subfig, letter, label_size)

Stamp a bold panel letter (a, b, c, …) in the top-left of a SubFigure.

align_title_to_labels

from fig_atlas.renderers import align_title_to_labels

align_title_to_labels(ax, title=None)

Start a left-aligned axes title at the axes' outer left edge.

A horizontal bar chart with long category names has its tick labels far left of the spine; a title at x=0 then starts over the bars, and one placed by a typed axes fraction (set_x(-1.25)) drifts whenever the labels or the axes width change. This measures the left edge of the tick labels and axis title (the title itself left out) and moves the title there, so the title -- and a panel tag placed on the outer box -- line up with the labels. title defaults to the axes' left title (what set_title(..., loc="left") sets), else its centre title. Call after the axes are fully drawn; returns the title.

readable_text_color

from fig_atlas.renderers.text_color import readable_text_color

readable_text_color(background, light='white', dark='#111827')

light or dark, whichever contrasts more with background.

Numbers in legends

A number a legend quotes is recorded by the script and written as {name} in the figure spec (AGENTS.md 4.5).

claims.record

from fig_atlas import claims

claims.record(name, value, source='')

Register a number this run computed; returns value unchanged.

claims.record("n_anchors", int(n), source="promoter DA peaks, padj < 0.05") lets the figure spec write {n_anchors:,} (Python format: {share:.0%}, {p:.2g}); publish fills it in and the value is kept in the figure's provenance record. name must be a Python identifier; source says what was counted.

claims.use_from

from fig_atlas import claims

claims.use_from(project, figure=1, names=None, root=None)

Take numbers another figure recorded (from its published provenance record), so a figure quoting them -- a summary schematic, a legend that refers back -- shows the same values. Reading the record makes it an input of this build, so this figure goes stale when those numbers change. Raises if that figure has not been built with a record.

claims.scan

from fig_atlas import claims

claims.scan(text)

Numbers typed into text (placeholders excluded), each classified.

Statistics

Stars and brackets come from these tests; the table is published with the figure (AGENTS.md 4.6).

stats.compare_to_control

from fig_atlas import stats

stats.compare_to_control(groups, control, test='welch', min_control_n=2)

Each group against control, in the order of groups.

Columns: group, control, test, n, n_control, mean, mean_control, statistic, p, note. A group with no values gets a row with p NaN; with fewer than min_control_n control values every p is NaN.

stats.two_group

from fig_atlas import stats

stats.two_group(a, b, test='welch')

Test b against a; returns (statistic, p, note). The statistic is b relative to a: positive when b is higher (for Mann-Whitney, U of b).

When both groups are constant there is nothing to test: p = 1 with note 'both groups constant'. A test that raises gives p = 1 with note 'test failed'. A t-test with a single value in a group gives NaN.

stats.anova

from fig_atlas import stats

stats.anova(groups)

One-way ANOVA over the non-empty groups; (F, p), NaN when it cannot be computed (fewer than two groups, or every group constant).

stats.adjust

from fig_atlas import stats

stats.adjust(p, method='none')

Multiple-testing correction over one family of P values. NaNs are left out of the family and stay NaN. Methods: none, bonferroni, holm, bh (Benjamini-Hochberg false discovery rate).

stats.stars

from fig_atlas import stats

stats.stars(p, thresholds=None, ns='')

Symbol for P value p: the first [threshold, symbol] pair (ascending thresholds) with p < threshold; ns otherwise or when p is NaN.

stats.describe

from fig_atlas import stats

stats.describe(test, correction='none', thresholds=None, against='control', family='')

Legend phrase for the stars, from the same settings that drew them: "Welch's t-test versus control (*P < 0.05, **P < 0.01, ***P < 0.001)"; with a correction, "..., Benjamini-Hochberg-adjusted (*adjusted P < 0.05, ...)".

stats.record

from fig_atlas import stats

stats.record(df, name, title='', note='')

Register a test table for the figure saved next; returns df. A name used twice before one save gets a numbered suffix (_2, ...).

Source data

The values each panel plots, published as CSV next to the figure.

source_data.add

from fig_atlas import source_data

source_data.add(df, panel, title='', name='', note='')

Register the table panel panel is drawn from; returns df.

source_data.add(table, panel="a", title="log2FC per gene") before the figure is saved: publish writes it as CSV into figure_NN_source_data/ with an index, the per-panel data many journals ask for. Pass the values as plotted (after filtering and renaming), not the raw input.

Publishing

A figure spec describes one figure; fa build runs the script, then publish and verify_atlas.

FigureSpec

from fig_atlas import FigureSpec

FigureSpec(number, slug, title, source_stem, primary_code, panel_notes=<factory>, statistics_tables=<factory>, claim_boundary='')

Declarative description of one manuscript figure.

Separates what a figure is (metadata, relationships, claim boundary) from how it is generated (analysis scripts) and where it lives (publish). One spec file per figure (figure_specs/.../figure_NN_<project>.py) holds one SPEC = FigureSpec(...); publish copies the files and writes the figure's README from it.

  • number, slug: the published folder is figure_NN_<slug>.
  • title: the figure's title in its README.
  • source_stem: the figure the script saved, without extension, relative to the output root (<domain>/<category>/<project>/figures/figure1_x).
  • primary_code: the analysis script, repository-relative.
  • panel_notes: one legend text per panel.
  • statistics_tables: test tables to publish with the figure (tables registered with stats.record are added on their own).
  • claim_boundary: what the figure does and does not show.

title, panel_notes and claim_boundary may hold live numbers, {name} or {name:,}, filled from claims.record at publish.

publish

from fig_atlas import publish

publish(spec, config)

Copy source figure files to a clean output folder and write a panel README.

Source files are resolved as {output_root}/{spec.source_stem}.{ext}, i.e. source_stem is relative to the configured output_root (OUTPUT_ROOT). Raises FileNotFoundError if any expected format file is missing.

verify_atlas

from fig_atlas import verify_atlas

verify_atlas(config)

Check that all figure folders, format files, and README files exist and are non-empty.

write_report

from fig_atlas import write_report

write_report(rows, out_path)

Write CSV report and return the number of failures (WARN rows don't count).

Parameters of your own code

A renderer declares its parameters in a SCHEMA, a script the keys it reads itself in a LAYOUT_SCHEMA; fa param and fa lint read them.

Param

from fig_atlas.schema import Param

Param(name, type, default=None, unit='', role='', tune='', range=None, aliases=(), open_keys=False)

One parameter a renderer or script reads from the config.

type is float, int, bool, str, list, dict, "color", "cmap" or a tuple of allowed values. default is the value used when the config has none (a number, a StyleRef to a style preset key, or CODE_DEFAULT). unit, role and tune are the three parts of the config comment (AGENTS.md 1.6): fa param scaffold writes them next to the key. range is (min, max), aliases old names still accepted, and open_keys marks a dict whose keys are free (rename maps, per-group colours).

Schema

from fig_atlas.schema import Schema

Schema(name, params=<factory>, extends=<factory>)

The parameters of one renderer (or, as LAYOUT_SCHEMA, the keys an analysis script reads itself): a list of Param plus shared groups in extends (e.g. typography()). fa param and fa lint read it, and uses_schema checks the values a renderer receives against it.

uses_schema

from fig_atlas.schema import uses_schema

uses_schema(schema)

Decorate a renderer whose signature has a params argument: it receives a BoundParams. A _style entry in params (merged figures/plots/typography config) feeds StyleRef defaults.

typography

from fig_atlas.schema import typography

typography(**fallbacks)

The shared text sizes a renderer reads, e.g. typography(title_size=13, tick_label_size=8): each defaults to the style preset's typography: block (final-size presets such as styles/print.yaml define it) and, where a preset has none, to the renderer's own historical default given here (CODE_DEFAULT: computed in the code) -- so projects without the block draw exactly as before.

StyleRef

from fig_atlas.schema import StyleRef

StyleRef(path, fallback=None)

A default taken from the merged style/config (figures.title_size), with fallback when no preset sets it.

CODE_DEFAULT

from fig_atlas.schema import CODE_DEFAULT

CODE_DEFAULT

Schema default meaning "whatever the renderer's own call passes": for defaults derived from other values (a_sz * 0.8) or that differ between code branches. A missing key then behaves exactly as before.

Reading provenance

What a published figure was built from; fa status, fa impact and fa lineage print the same.

published_records

from fig_atlas.provenance import published_records

published_records(root, figures_root=None)

(record path, record) for every published figure record under figures_root (default: all of outputs/).

check_freshness

from fig_atlas.provenance import check_freshness

check_freshness(root, record, cache=None)

Why a figure is stale: each recorded input, code file or config whose content no longer matches (empty list = fresh). Content hashes, not timestamps: touching a file without changing it keeps the figure fresh.

impacted_by

from fig_atlas.provenance import impacted_by

impacted_by(root, target)

Published figures whose build read target (a file, or every file under a folder) as an input, config or code.

lineage

from fig_atlas.provenance import lineage

lineage(record)

A figure's upstream tree, as text lines: outputs <- build run <- config and styles <- code <- inputs grouped by data bundle (with its exporter).