Source code for polyzymd.compare.binding_free_energy_formatters
"""Output formatters for binding free energy comparison results.
Provides console table, Markdown, and JSON output for BindingFreeEnergyResult.
"""
from __future__ import annotations
import json
import math
from typing import Optional
from polyzymd.compare.results.binding_free_energy import (
BindingFreeEnergyResult,
FreeEnergyConditionSummary,
FreeEnergyEntry,
FreeEnergyPairwiseEntry,
)
from polyzymd.core.experimental import prefix_experimental_output
[docs]
def format_bfe_console_table(result: BindingFreeEnergyResult) -> str:
"""Format a BindingFreeEnergyResult as a console-friendly ASCII table.
Parameters
----------
result : BindingFreeEnergyResult
Comparison result to format.
Returns
-------
str
ASCII table string.
"""
lines: list[str] = []
u = result.units
# Header
lines.append("")
lines.append(f"Binding Free Energy Comparison: {result.name}")
lines.append("=" * 80)
lines.append(f"Formula : {result.formula}")
lines.append(f"Units : {u}")
lines.append(f"Equilibration: {result.equilibration_time}")
if result.surface_exposure_threshold is not None:
lines.append(f"SASA threshold: {result.surface_exposure_threshold:.2f}")
if result.mixed_temperatures:
temp_str = ", ".join(
f"{t} K ({', '.join(labels)})" for t, labels in result.temperature_groups.items()
)
lines.append(f"Temperatures: {temp_str}")
lines.append("Note: pairwise statistics suppressed for cross-temperature pairs.")
lines.append("")
# Per-condition summary table
lines.append("ΔG_sel Summary by Condition (sign: negative = preferential binding)")
lines.append("-" * 80)
for summary in result.conditions:
_format_condition_block(lines, summary, u)
# Pairwise section
if result.pairwise_comparisons:
same_t_pairs = [p for p in result.pairwise_comparisons if not p.cross_temperature]
cross_t_pairs = [p for p in result.pairwise_comparisons if p.cross_temperature]
if same_t_pairs:
lines.append("")
lines.append("Pairwise ΔΔG Differences (ΔG_sel,B − ΔG_sel,A)")
lines.append("-" * 80)
_format_pairwise_block(lines, same_t_pairs, u)
if cross_t_pairs:
lines.append("")
lines.append(
f"Cross-temperature pairs ({len(cross_t_pairs)} entries) — statistics suppressed"
)
lines.append("(ΔG_sel values are shown for reference but cannot be compared directly)")
lines.append("")
return "\n".join(lines)
def _format_condition_block(
lines: list[str],
summary: FreeEnergyConditionSummary,
units: str,
) -> None:
"""Format one condition's ΔG_sel entries as a sub-table."""
lines.append(
f"\n {summary.label} (T = {summary.temperature_K} K, n = {summary.n_replicates})"
)
lines.append(f" {'Polymer':<12} {'AA Group':<22} {'ΔG_sel':>10} {'±σ':>10} {'N_rep':>5}")
lines.append(" " + "-" * 63)
if not summary.entries:
lines.append(" (no data)")
return
for entry in sorted(summary.entries, key=lambda e: (e.polymer_type, e.protein_group)):
if entry.delta_G is None:
dg_str = " N/A"
unc_str = " N/A"
else:
dg_str = f"{entry.delta_G:>+10.3f}"
unc_str = (
f"{entry.delta_G_uncertainty:>+10.3f}"
if entry.delta_G_uncertainty is not None
else " --"
)
lines.append(
f" {entry.polymer_type:<12} {entry.protein_group:<22} "
f"{dg_str} {unc_str} {entry.n_replicates:>5}"
)
lines.append("")
def _format_pairwise_block(
lines: list[str],
pairs: list[FreeEnergyPairwiseEntry],
units: str,
) -> None:
"""Format pairwise comparison entries."""
# Group by (condition_a, condition_b)
pair_groups: dict[tuple[str, str], list[FreeEnergyPairwiseEntry]] = {}
for p in pairs:
key = (p.condition_a, p.condition_b)
pair_groups.setdefault(key, []).append(p)
for (cond_a, cond_b), entries in sorted(pair_groups.items()):
lines.append(f"\n {cond_a} → {cond_b}")
lines.append(
f" {'Polymer':<12} {'AA Group':<22} {'ΔG_sel,A':>10} {'ΔG_sel,B':>10} "
f"{'ΔΔG_B−A':>10} {'p-value':>10}"
)
lines.append(" " + "-" * 77)
for e in sorted(entries, key=lambda x: (x.polymer_type, x.protein_group)):
dg_a = f"{e.delta_G_a:>+10.3f}" if e.delta_G_a is not None else " N/A"
dg_b = f"{e.delta_G_b:>+10.3f}" if e.delta_G_b is not None else " N/A"
ddg = f"{e.delta_delta_G:>+10.3f}" if e.delta_delta_G is not None else " N/A"
pval = f"{e.p_value:>10.4f}" if e.p_value is not None else " --"
lines.append(f" {e.polymer_type:<12} {e.protein_group:<22} {dg_a} {dg_b} {ddg} {pval}")
[docs]
def format_bfe_markdown(result: BindingFreeEnergyResult) -> str:
"""Format a BindingFreeEnergyResult as Markdown.
Parameters
----------
result : BindingFreeEnergyResult
Comparison result to format.
Returns
-------
str
Markdown-formatted string.
"""
lines: list[str] = []
u = result.units
lines.append(f"# Binding Free Energy Comparison: {result.name}")
lines.append("")
lines.append(f"**Formula:** `{result.formula}` ")
lines.append(f"**Units:** {u} ")
lines.append(f"**Equilibration:** {result.equilibration_time} ")
if result.surface_exposure_threshold is not None:
lines.append(f"**SASA threshold:** {result.surface_exposure_threshold:.2f} ")
if result.mixed_temperatures:
temp_str = ", ".join(
f"{t} K ({', '.join(labels)})" for t, labels in result.temperature_groups.items()
)
lines.append(f"**Temperatures:** {temp_str} ")
lines.append("> **Note:** Pairwise statistics are suppressed for cross-temperature pairs.")
lines.append("")
for summary in result.conditions:
lines.append(f"## {summary.label}")
lines.append(f"Temperature: {summary.temperature_K} K | Replicates: {summary.n_replicates}")
lines.append("")
if not summary.entries:
lines.append("_No data available._")
lines.append("")
continue
lines.append(f"| Polymer | AA Group | ΔG_sel ({u}) | ±σ | N |")
lines.append("|---------|----------|----------:|---:|---|")
for entry in sorted(summary.entries, key=lambda e: (e.polymer_type, e.protein_group)):
if entry.delta_G is None:
dg_str = "N/A"
unc_str = "N/A"
else:
dg_str = f"{entry.delta_G:+.3f}"
unc_str = (
f"{entry.delta_G_uncertainty:.3f}"
if entry.delta_G_uncertainty is not None
else "--"
)
lines.append(
f"| {entry.polymer_type} | {entry.protein_group} | "
f"{dg_str} | {unc_str} | {entry.n_replicates} |"
)
lines.append("")
# Pairwise table
same_t_pairs = [p for p in result.pairwise_comparisons if not p.cross_temperature]
if same_t_pairs:
lines.append("## Pairwise Comparisons")
lines.append("")
pair_groups: dict[tuple[str, str], list[FreeEnergyPairwiseEntry]] = {}
for p in same_t_pairs:
key = (p.condition_a, p.condition_b)
pair_groups.setdefault(key, []).append(p)
for (cond_a, cond_b), entries in sorted(pair_groups.items()):
lines.append(f"### {cond_a} → {cond_b}")
lines.append("")
lines.append(
f"| Polymer | AA Group | ΔG_sel,A ({u}) | ΔG_sel,B ({u}) | ΔΔG_B−A ({u}) | p-value |"
)
lines.append("|---------|----------|----------:|----------:|----------:|--------:|")
for e in sorted(entries, key=lambda x: (x.polymer_type, x.protein_group)):
dg_a = f"{e.delta_G_a:+.3f}" if e.delta_G_a is not None else "N/A"
dg_b = f"{e.delta_G_b:+.3f}" if e.delta_G_b is not None else "N/A"
ddg = f"{e.delta_delta_G:+.3f}" if e.delta_delta_G is not None else "N/A"
pval = f"{e.p_value:.4f}" if e.p_value is not None else "--"
lines.append(
f"| {e.polymer_type} | {e.protein_group} | {dg_a} | {dg_b} | {ddg} | {pval} |"
)
lines.append("")
return "\n".join(lines)
[docs]
def format_bfe_json(result: BindingFreeEnergyResult) -> str:
"""Format a BindingFreeEnergyResult as JSON.
Parameters
----------
result : BindingFreeEnergyResult
Comparison result to format.
Returns
-------
str
JSON string.
"""
return result.model_dump_json(indent=2)
[docs]
def format_bfe_result(
result: BindingFreeEnergyResult,
format: str = "table",
) -> str:
"""Format a BindingFreeEnergyResult in the requested format.
Parameters
----------
result : BindingFreeEnergyResult
Comparison result to format.
format : str
Output format: "table" (default), "markdown", or "json".
Returns
-------
str
Formatted string.
Raises
------
ValueError
If format is not recognized.
"""
if format == "table":
formatted = format_bfe_console_table(result)
elif format == "markdown":
formatted = format_bfe_markdown(result)
elif format == "json":
formatted = format_bfe_json(result)
else:
raise ValueError(f"Unknown format '{format}'. Use 'table', 'markdown', or 'json'.")
return prefix_experimental_output(formatted, ("binding_free_energy",), format)