Source code for qca.reporting.export

"""Markdown and LaTeX export helpers."""

from __future__ import annotations

from pathlib import Path
from typing import Any

import pandas as pd

from qca.reporting.reproducibility import collect_reproducibility_metadata


[docs] def generate_markdown_report( obj: Any, *, path: str | Path | None = None, title: str | None = None, include_metadata: bool = True, max_rows: int = 20, ) -> str: """Generate an automated Markdown report for a model, fit result, or sweep.""" report_title = title or _default_title(obj) lines = [f"# {report_title}", ""] if include_metadata: lines.extend(["## Reproducibility Metadata", ""]) metadata = collect_reproducibility_metadata(obj) for key, value in metadata.items(): lines.append(f"- `{key}`: {_format_markdown_value(value)}") lines.append("") if hasattr(obj, "settings"): lines.extend(["## Settings", ""]) for key, value in obj.settings.items(): lines.append(f"- `{key}`: {_format_markdown_value(value)}") lines.append("") if hasattr(obj, "summary") and not isinstance(obj.summary, pd.DataFrame): try: summary_value = obj.summary() except TypeError: summary_value = None if isinstance(summary_value, str): lines.extend(["## Summary", "", summary_value, ""]) if hasattr(obj, "stability"): lines.extend(["## Stability", ""]) for key, value in obj.stability.items(): lines.append(f"- `{key}`: {_format_markdown_value(value)}") lines.append("") for section, table in _report_tables(obj): if table.empty: continue lines.extend([f"## {section}", ""]) lines.append(_dataframe_to_markdown(table.head(max_rows))) if len(table) > max_rows: lines.append(f"\n... {len(table) - max_rows} more rows") lines.append("") markdown = "\n".join(lines).rstrip() + "\n" if path is not None: Path(path).write_text(markdown, encoding="utf-8") return markdown
[docs] def to_latex_table( obj: Any, *, table: str = "solutions", path: str | Path | None = None, caption: str | None = None, label: str | None = None, max_rows: int | None = None, index: bool = False, ) -> str: """Export one object table as a simple LaTeX tabular environment.""" df = _resolve_table(obj, table) if max_rows is not None: df = df.head(max_rows) latex = _dataframe_to_latex(df, caption=caption, label=label, index=index) if path is not None: Path(path).write_text(latex, encoding="utf-8") return latex
def _report_tables(obj: Any) -> list[tuple[str, pd.DataFrame]]: tables: list[tuple[str, pd.DataFrame]] = [] for title, attr in ( ("Solutions", "solutions"), ("Truth Table", "truth_table"), ("Case Coverage", "case_coverage"), ("Condition Schema", "condition_schema"), ("Sweep Summary", "summary_df"), ("Condition Ranking", "condition_ranking"), ("Feature Importance", "feature_importance"), ("Calibration Proposals", "calibration_proposals"), ("Candidate Models", "candidate_models"), ("Pareto Frontier", "pareto_models"), ): if hasattr(obj, attr): value = getattr(obj, attr) if isinstance(value, pd.DataFrame): tables.append((title, value.copy())) if hasattr(obj, "data") and not tables: value = obj.data if isinstance(value, pd.DataFrame): tables.append(("Data Preview", value.copy())) return tables def _resolve_table(obj: Any, table: str) -> pd.DataFrame: if isinstance(obj, pd.DataFrame): return obj.copy() if hasattr(obj, table): value = getattr(obj, table) if isinstance(value, pd.DataFrame): return value.copy() if table == "summary" and hasattr(obj, "summary"): value = obj.summary() if isinstance(value, pd.DataFrame): return value.copy() available = [ name for name in ( "solutions", "truth_table", "case_coverage", "condition_schema", "summary_df", "condition_ranking", "feature_importance", "calibration_proposals", "candidate_models", "pareto_models", ) if hasattr(obj, name) ] raise ValueError(f"Unknown table {table!r}. Available tables: {available}") def _dataframe_to_markdown(df: pd.DataFrame) -> str: if df.empty: return "No rows." columns = [str(c) for c in df.columns] rows = [ [_format_markdown_value(value) for value in row] for row in df.itertuples(index=False, name=None) ] header = "| " + " | ".join(columns) + " |" separator = "| " + " | ".join("---" for _ in columns) + " |" body = ["| " + " | ".join(row) + " |" for row in rows] return "\n".join([header, separator, *body]) def _dataframe_to_latex( df: pd.DataFrame, *, caption: str | None, label: str | None, index: bool, ) -> str: view = df.reset_index() if index else df.copy() columns = [str(column) for column in view.columns] align = "l" * max(len(columns), 1) lines: list[str] = [] if caption is not None or label is not None: lines.append("\\begin{table}") lines.append("\\centering") lines.append(f"\\begin{{tabular}}{{{align}}}") lines.append("\\toprule") lines.append(" & ".join(_escape_latex(column) for column in columns) + r" \\") lines.append("\\midrule") for row in view.itertuples(index=False, name=None): lines.append( " & ".join(_escape_latex(_format_latex_value(value)) for value in row) + r" \\" ) lines.append("\\bottomrule") lines.append("\\end{tabular}") if caption is not None: lines.append(f"\\caption{{{_escape_latex(caption)}}}") if label is not None: lines.append(f"\\label{{{_escape_latex(label)}}}") if caption is not None or label is not None: lines.append("\\end{table}") return "\n".join(lines) + "\n" def _format_markdown_value(value: Any) -> str: if value is None or value is pd.NA: return "" if isinstance(value, float): if pd.isna(value): return "" return f"{value:.4f}" if isinstance(value, (dict, list, tuple)): return str(value).replace("|", "\\|") return str(value).replace("|", "\\|") def _format_latex_value(value: Any) -> str: if value is None or value is pd.NA: return "" if isinstance(value, float): if pd.isna(value): return "" return f"{value:.4f}" return str(value) def _escape_latex(value: str) -> str: replacements = { "\\": r"\textbackslash{}", "&": r"\&", "%": r"\%", "$": r"\$", "#": r"\#", "_": r"\_", "{": r"\{", "}": r"\}", "~": r"\textasciitilde{}", "^": r"\textasciicircum{}", } return "".join(replacements.get(char, char) for char in str(value)) def _default_title(obj: Any) -> str: if hasattr(obj, "qca_type"): return f"{obj.qca_type} Report" return f"{type(obj).__name__} Report" __all__ = [ "generate_markdown_report", "to_latex_table", ]