Source code for chaotic_pfc.analysis.latex_export

"""LaTeX table export for chaotic-PFC analysis results.

Every exporter accepts a ``lang`` parameter (``"pt"`` or ``"en"``)
that drives column headers, captions, and filter names via
:func:`~chaotic_pfc._i18n.t`.

All tables use ``booktabs`` rules and are wrapped in a ``table``
float (``longtable`` for the full ranking). Output encoding is
UTF-8.
"""

from __future__ import annotations

import math
from pathlib import Path
from typing import TYPE_CHECKING

from chaotic_pfc._i18n import t

if TYPE_CHECKING:
    from .stats import ConfigRank, KaiserBetaOptimal, SweetSpot


def _fmt(v: float | int, decimals: int = 3) -> str:
    """Format a number with *decimals* places, strip trailing zeros."""
    if isinstance(v, float) and (math.isnan(v) or math.isinf(v)):
        return "---"
    if isinstance(v, int) or v == int(v):
        return str(int(v))
    s = f"{v:.{decimals}f}"
    if "." in s:
        s = s.rstrip("0").rstrip(".")
    return s


def _ci_str(low: float | None, high: float | None) -> str:
    """CI as ``[low, high]`` or ``---`` if None."""
    if low is None or high is None:
        return "---"
    return f"[{_fmt(low, 4)}, {_fmt(high, 4)}]"


def _window_label(entry: ConfigRank | SweetSpot, lang: str) -> str:
    """Format a window name, handling Kaiser with beta."""
    win = entry["window"]
    if win.startswith("kaiser_"):
        beta_str = win[len("kaiser_") :]
        base = t("analysis.window.kaiser", lang=lang)
        return f"{base}($\\beta={beta_str}$)"
    from .sweep import WINDOW_DISPLAY_NAMES

    return WINDOW_DISPLAY_NAMES.get(win, win.capitalize())


def _filter_label(ft: str, lang: str) -> str:
    """Filter type display name via i18n."""
    return t(f"analysis.filter.{ft}", lang=lang)


def _render_tex(
    columns: list[str],
    rows: list[list[str]],
    caption: str,
    label: str | None,
    *,
    use_longtable: bool = False,
) -> str:
    """Build a complete LaTeX table string.

    Parameters
    ----------
    columns
        Header row cell contents.
    rows
        Data rows (each a list of strings).
    caption
        Caption text.
    label
        Optional ``\\label{...}`` value.
    use_longtable
        If True, uses ``longtable`` instead of ``tabular`` + ``table`` float.
    """
    n_cols = len(columns)
    col_spec = "l" + "r" * (n_cols - 1)

    lines: list[str] = []
    if not use_longtable:
        lines.append("\\begin{table}[htbp]")
        lines.append("  \\centering")
    lines.append(f"  \\caption{{{caption}}}")
    if label:
        lines.append(f"  \\label{{{label}}}")
    if not use_longtable:
        lines.append("  \\footnotesize")
        lines.append("  \\setlength{\\tabcolsep}{4pt}")
        lines.append("  \\resizebox{\\textwidth}{!}{%")
    lines.append("  \\begin{longtable}" if use_longtable else f"  \\begin{{tabular}}{{{col_spec}}}")
    lines.append("    \\toprule")
    header = " & ".join(f"{c}" for c in columns) + " \\\\"
    lines.append(f"    {header}")
    lines.append("    \\midrule")
    for row in rows:
        row_str = " & ".join(str(cell) for cell in row) + " \\\\"
        lines.append(f"    {row_str}")
    lines.append("    \\bottomrule")
    if use_longtable:
        lines.append("  \\end{longtable}")
    else:
        lines.append("  \\end{tabular}")
        lines.append("  }")  # close \resizebox
        lines.append("\\end{table}")
    return "\n".join(lines) + "\n"


[docs] def export_top_k_table( top_k_data: dict[str, list[ConfigRank]], output_path: str | Path, caption_key: str | None = None, label: str | None = None, lang: str | None = None, ) -> Path: """Export Categoria A — Top-3 per filter (chaotic area only). Parameters ---------- top_k_data ``{filter_type: [ConfigRank, ...]}`` from :func:`~.stats.top_k_per_filter`. output_path Destination ``.tex`` file. caption_key i18n key for the caption. Defaults to ``analysis.tables.top_k.caption``. label LaTeX label (without ``\\label`` wrapper). lang ``"pt"`` or ``"en"``. Defaults to the active locale. Returns ------- Path The path written. """ if caption_key is None: caption_key = "analysis.tables.top_k.caption" caption = t(caption_key, lang=lang) columns = [ t("analysis.tables.col.filter", lang=lang), t("analysis.tables.col.rank", lang=lang), t("analysis.tables.col.window", lang=lang), t("analysis.tables.col.n_chaotic", lang=lang), t("analysis.tables.col.pct_chaotic", lang=lang), ] rows: list[list[str]] = [] for ft in ["lowpass", "highpass", "bandpass", "bandstop"]: entries = top_k_data.get(ft, []) for entry in entries: rows.append( [ _filter_label(ft, lang or "pt"), str(entry["rank"]), _window_label(entry, lang or "pt"), str(entry["n_chaotic"]), f"{entry['pct_chaotic']:.1f}\\%", ] ) tex = _render_tex(columns, rows, caption, label) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(tex, encoding="utf-8") return output
[docs] def export_extended_top_k_table( top_k_data: dict[str, list[ConfigRank]], output_path: str | Path, caption_key: str | None = None, label: str | None = None, lang: str | None = None, ) -> Path: """Export Categoria B — Top-3 per filter with extended λ_max statistics. Parameters ---------- top_k_data ``{filter_type: [ConfigRank, ...]}`` from :func:`~.stats.top_k_per_filter`. output_path Destination ``.tex`` file. caption_key i18n key for the caption. Defaults to ``analysis.tables.top_k_extended.caption``. label LaTeX label. lang ``"pt"`` or ``"en"``. Returns ------- Path """ if caption_key is None: caption_key = "analysis.tables.top_k_extended.caption" caption = t(caption_key, lang=lang) columns = [ t("analysis.tables.col.filter", lang=lang), t("analysis.tables.col.rank", lang=lang), t("analysis.tables.col.window", lang=lang), t("analysis.tables.col.n_chaotic", lang=lang), t("analysis.tables.col.pct_chaotic_finite", lang=lang), t("analysis.tables.col.lmax_mean", lang=lang), t("analysis.tables.col.lmax_max", lang=lang), t("analysis.tables.col.lmax_std", lang=lang), t("analysis.tables.col.lmax_ci95", lang=lang), ] rows: list[list[str]] = [] for ft in ["lowpass", "highpass", "bandpass", "bandstop"]: entries = top_k_data.get(ft, []) for entry in entries: rows.append( [ _filter_label(ft, lang or "pt"), str(entry["rank"]), _window_label(entry, lang or "pt"), str(entry["n_chaotic"]), f"{entry['pct_chaotic_finite']:.1f}\\%", _fmt(entry["lmax_mean"], 4), _fmt(entry["lmax_max"], 4), _fmt(entry["lmax_std"], 4), _ci_str(entry["lmax_ci_95_low"], entry["lmax_ci_95_high"]), ] ) tex = _render_tex(columns, rows, caption, label) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(tex, encoding="utf-8") return output
[docs] def export_full_ranking_table( rank_data: list[ConfigRank], output_path: str | Path, caption_key: str | None = None, label: str | None = None, lang: str | None = None, ) -> Path: """Export Categoria C.1 — Full ranking (longtable). Parameters ---------- rank_data Sorted list from :func:`~.stats.rank_configurations`. output_path Destination ``.tex`` file. caption_key i18n key for the caption. Defaults to ``analysis.tables.full_ranking.caption``. label LaTeX label. lang ``"pt"`` or ``"en"``. Returns ------- Path """ if caption_key is None: caption_key = "analysis.tables.full_ranking.caption" caption = t(caption_key, lang=lang) columns = [ t("analysis.tables.col.rank", lang=lang), t("analysis.tables.col.filter", lang=lang), t("analysis.tables.col.window", lang=lang), t("analysis.tables.col.n_chaotic", lang=lang), t("analysis.tables.col.pct_chaotic_finite", lang=lang), t("analysis.tables.col.lmax_mean", lang=lang), t("analysis.tables.col.lmax_max", lang=lang), t("analysis.tables.col.lmax_std", lang=lang), t("analysis.tables.col.lmax_ci95", lang=lang), ] rows: list[list[str]] = [] for entry in rank_data: rows.append( [ str(entry["rank"]), _filter_label(entry["filter_type"], lang or "pt"), _window_label(entry, lang or "pt"), str(entry["n_chaotic"]), f"{entry['pct_chaotic_finite']:.1f}\\%", _fmt(entry["lmax_mean"], 4), _fmt(entry["lmax_max"], 4), _fmt(entry["lmax_std"], 4), _ci_str(entry["lmax_ci_95_low"], entry["lmax_ci_95_high"]), ] ) tex = _render_tex(columns, rows, caption, label, use_longtable=True) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(tex, encoding="utf-8") return output
[docs] def export_sweet_spots_table( sweet_spot_data: dict[str, SweetSpot], output_path: str | Path, caption_key: str | None = None, label: str | None = None, lang: str | None = None, ) -> Path: """Export Categoria C.2 — Sweet-spot points per filter type. Parameters ---------- sweet_spot_data ``{filter_type: SweetSpot}`` from :func:`~.stats.sweet_spot_per_filter`. output_path Destination ``.tex`` file. caption_key i18n key for the caption. Defaults to ``analysis.tables.sweet_spots.caption``. label LaTeX label. lang ``"pt"`` or ``"en"``. Returns ------- Path """ if caption_key is None: caption_key = "analysis.tables.sweet_spots.caption" caption = t(caption_key, lang=lang) columns = [ t("analysis.tables.col.filter", lang=lang), t("analysis.tables.col.window", lang=lang), t("analysis.tables.col.n_z", lang=lang), t("analysis.tables.col.omega_c", lang=lang), t("analysis.tables.col.lmax_max", lang=lang), t("analysis.tables.col.lmax_ci95", lang=lang), ] rows: list[list[str]] = [] for ft in ["lowpass", "highpass", "bandpass", "bandstop"]: spot = sweet_spot_data.get(ft) if spot is None: continue rows.append( [ _filter_label(ft, lang or "pt"), _window_label(spot, lang or "pt"), str(spot["n_z"]), _fmt(spot["omega_c"], 4), _fmt(spot["lmax"], 6), _ci_str(spot["lmax_ci_95_low"], spot["lmax_ci_95_high"]), ] ) tex = _render_tex(columns, rows, caption, label) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(tex, encoding="utf-8") return output
[docs] def export_kaiser_beta_optimal_table( beta_data: dict[str, KaiserBetaOptimal], output_path: str | Path, caption_key: str | None = None, label: str | None = None, lang: str | None = None, ) -> Path: """Export optimal Kaiser β per filter type. Parameters ---------- beta_data ``{filter_type: KaiserBetaOptimal}`` from :func:`~.stats.kaiser_beta_optimal`. output_path Destination ``.tex`` file. caption_key i18n key. Defaults to ``analysis.tables.kaiser_beta_optimal.caption``. label LaTeX label. lang ``"pt"`` or ``"en"``. Returns ------- Path """ if caption_key is None: caption_key = "analysis.tables.kaiser_beta_optimal.caption" caption = t(caption_key, lang=lang) columns = [ t("analysis.tables.col.filter", lang=lang), t("analysis.tables.col.beta", lang=lang), t("analysis.tables.col.n_chaotic", lang=lang), t("analysis.tables.col.pct_chaotic_finite", lang=lang), t("analysis.tables.col.lmax_mean", lang=lang), t("analysis.tables.col.lmax_max", lang=lang), ] rows: list[list[str]] = [] for ft in ["lowpass", "highpass", "bandpass", "bandstop"]: entry = beta_data.get(ft) if entry is None: continue rows.append( [ _filter_label(ft, lang or "pt"), _fmt(entry["beta"], 2), str(entry["n_chaotic"]), f"{entry['pct_chaotic_finite']:.1f}\\%", _fmt(entry["lmax_mean"], 4), _fmt(entry["lmax_max"], 4), ] ) tex = _render_tex(columns, rows, caption, label) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(tex, encoding="utf-8") return output
[docs] def export_consolidated_top_k_table( consolidated_top_k: dict[str, list[ConfigRank]], output_path: str | Path, caption_key: str | None = None, label: str | None = None, lang: str | None = None, ) -> Path: """Export consolidated top-k (Categoria A, Kaiser collapsed to best β). Parameters ---------- consolidated_top_k ``{filter_type: [ConfigRank, ...]}`` from :func:`~.stats.top_k_per_filter` run on consolidated sweeps. output_path Destination ``.tex`` file. caption_key i18n key. Defaults to ``analysis.tables.consolidated_top_k.caption``. label LaTeX label. lang ``"pt"`` or ``"en"``. Returns ------- Path """ if caption_key is None: caption_key = "analysis.tables.consolidated_top_k.caption" caption = t(caption_key, lang=lang) columns = [ t("analysis.tables.col.filter", lang=lang), t("analysis.tables.col.rank", lang=lang), t("analysis.tables.col.window", lang=lang), t("analysis.tables.col.n_chaotic", lang=lang), t("analysis.tables.col.pct_chaotic", lang=lang), ] rows: list[list[str]] = [] for ft in ["lowpass", "highpass", "bandpass", "bandstop"]: entries = consolidated_top_k.get(ft, []) for entry in entries: rows.append( [ _filter_label(ft, lang or "pt"), str(entry["rank"]), _window_label(entry, lang or "pt"), str(entry["n_chaotic"]), f"{entry['pct_chaotic']:.1f}\\%", ] ) tex = _render_tex(columns, rows, caption, label) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(tex, encoding="utf-8") return output
[docs] def export_consolidated_extended_table( consolidated_top_k: dict[str, list[ConfigRank]], output_path: str | Path, caption_key: str | None = None, label: str | None = None, lang: str | None = None, ) -> Path: """Export consolidated extended top-k (Categoria B). Parameters ---------- consolidated_top_k From :func:`~.stats.top_k_per_filter` on consolidated sweeps. output_path Destination ``.tex`` file. caption_key i18n key. Defaults to ``analysis.tables.consolidated_extended.caption``. label LaTeX label. lang ``"pt"`` or ``"en"``. Returns ------- Path """ if caption_key is None: caption_key = "analysis.tables.consolidated_extended.caption" caption = t(caption_key, lang=lang) columns = [ t("analysis.tables.col.filter", lang=lang), t("analysis.tables.col.rank", lang=lang), t("analysis.tables.col.window", lang=lang), t("analysis.tables.col.n_chaotic", lang=lang), t("analysis.tables.col.pct_chaotic_finite", lang=lang), t("analysis.tables.col.lmax_mean", lang=lang), t("analysis.tables.col.lmax_max", lang=lang), t("analysis.tables.col.lmax_std", lang=lang), t("analysis.tables.col.lmax_ci95", lang=lang), ] rows: list[list[str]] = [] for ft in ["lowpass", "highpass", "bandpass", "bandstop"]: entries = consolidated_top_k.get(ft, []) for entry in entries: rows.append( [ _filter_label(ft, lang or "pt"), str(entry["rank"]), _window_label(entry, lang or "pt"), str(entry["n_chaotic"]), f"{entry['pct_chaotic_finite']:.1f}\\%", _fmt(entry["lmax_mean"], 4), _fmt(entry["lmax_max"], 4), _fmt(entry["lmax_std"], 4), _ci_str(entry["lmax_ci_95_low"], entry["lmax_ci_95_high"]), ] ) tex = _render_tex(columns, rows, caption, label) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(tex, encoding="utf-8") return output
[docs] def export_consolidated_full_ranking( consolidated_rank: list[ConfigRank], output_path: str | Path, caption_key: str | None = None, label: str | None = None, lang: str | None = None, ) -> Path: """Export consolidated full ranking (Categoria C.1, longtable). Parameters ---------- consolidated_rank From :func:`~.stats.rank_configurations` on consolidated sweeps. output_path Destination ``.tex`` file. caption_key i18n key. Defaults to ``analysis.tables.consolidated_full_ranking.caption``. label LaTeX label. lang ``"pt"`` or ``"en"``. Returns ------- Path """ if caption_key is None: caption_key = "analysis.tables.consolidated_full_ranking.caption" caption = t(caption_key, lang=lang) columns = [ t("analysis.tables.col.rank", lang=lang), t("analysis.tables.col.filter", lang=lang), t("analysis.tables.col.window", lang=lang), t("analysis.tables.col.n_chaotic", lang=lang), t("analysis.tables.col.pct_chaotic_finite", lang=lang), t("analysis.tables.col.lmax_mean", lang=lang), t("analysis.tables.col.lmax_max", lang=lang), t("analysis.tables.col.lmax_std", lang=lang), t("analysis.tables.col.lmax_ci95", lang=lang), ] rows: list[list[str]] = [] for entry in consolidated_rank: rows.append( [ str(entry["rank"]), _filter_label(entry["filter_type"], lang or "pt"), _window_label(entry, lang or "pt"), str(entry["n_chaotic"]), f"{entry['pct_chaotic_finite']:.1f}\\%", _fmt(entry["lmax_mean"], 4), _fmt(entry["lmax_max"], 4), _fmt(entry["lmax_std"], 4), _ci_str(entry["lmax_ci_95_low"], entry["lmax_ci_95_high"]), ] ) tex = _render_tex(columns, rows, caption, label, use_longtable=True) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(tex, encoding="utf-8") return output