"""Pretty-printing utilities."""
from __future__ import annotations
import enum
import math
import string
from enum import Enum
from fractions import Fraction
from math import pi
from typing import TYPE_CHECKING, SupportsFloat
# `assert_never` introduced in Python 3.11
from typing_extensions import assert_never
from graphix import command
from graphix.fundamentals import AbstractMeasurement, Axis, Plane, Sign, angle_to_rad, rad_to_angle
from graphix.measurements import BlochMeasurement, PauliMeasurement
from graphix.parameter import AffineExpression
if TYPE_CHECKING:
from collections.abc import Container, Iterable, Mapping, Sequence
from collections.abc import Set as AbstractSet
from graphix.command import Node
from graphix.flow.core import PauliFlow, XZCorrections
from graphix.fundamentals import Angle
from graphix.pattern import Pattern
[docs]
def angle_to_str(
angle: Angle, output: OutputFormat, max_denominator: int = 1000, multiplication_sign: bool = False
) -> str:
r"""
Return a string representation of an angle given in units of π.
- If the angle is a "simple" fraction of π (within the given max_denominator and a small tolerance),
it returns a fractional string, e.g. "π/2", "2π", or "-3π/4".
- Otherwise, it returns the angle in radians (angle * π) formatted to two decimal places.
Parameters
----------
angle : float
The angle in multiples of π (e.g., 0.5 means π/2).
output : OutputFormat
Desired formatting style: Unicode (π symbol), LaTeX (\pi), or ASCII ("pi").
max_denominator : int, optional
Maximum denominator for detecting a simple fraction (default: 1000).
multiplication_sign : bool
Optional (default: ``False``).
If ``True``, the multiplication sign is made explicit between the
numerator and π:
``2×π`` in Unicode, ``2 \times \pi`` in LaTeX, and ``2*pi`` in ASCII.
If ``False``, the multiplication sign is implicit:
``2π`` in Unicode, ``2\pi`` in LaTeX, ``2pi`` in ASCII.
Returns
-------
str
The formatted angle.
"""
frac = Fraction(angle).limit_denominator(max_denominator)
if not math.isclose(angle, float(frac)):
rad = angle_to_rad(angle)
return f"{rad}"
num, den = frac.numerator, frac.denominator
sign = "-" if num < 0 else ""
num = abs(num)
if output == OutputFormat.LaTeX:
pi = r"\pi"
def mkfrac(num: str, den: str) -> str:
return rf"\frac{{{num}}}{{{den}}}"
mul = r" \times "
else:
pi = "π" if output == OutputFormat.Unicode else "pi"
def mkfrac(num: str, den: str) -> str:
return f"{num}/{den}"
mul = "×" if output == OutputFormat.Unicode else "*"
if not multiplication_sign:
mul = ""
if den == 1:
match num:
case 0:
return "0"
case 1:
return f"{sign}{pi}"
case _:
return f"{sign}{num}{mul}{pi}"
den_str = f"{den}"
num_str = pi if num == 1 else f"{num}{mul}{pi}"
return f"{sign}{mkfrac(num_str, den_str)}"
def domain_to_str(domain: set[Node]) -> str:
"""Return the string representation of a domain."""
return f"{{{','.join(str(node) for node in domain)}}}"
SUBSCRIPTS = str.maketrans(string.digits, "₀₁₂₃₄₅₆₇₈₉")
SUPERSCRIPTS = str.maketrans(string.digits, "⁰¹²³⁴⁵⁶⁷⁸⁹")
def affine_expression_to_str(expr: AffineExpression, output: OutputFormat) -> str:
"""Return the string representation of an affine expression."""
result = str(expr.x)
if expr.a != 1:
a = angle_to_str(rad_to_angle(expr.a), output)
match output:
case OutputFormat.LaTeX:
mul = r" \times "
case OutputFormat.Unicode:
mul = "×"
case OutputFormat.ASCII:
mul = "*"
result = f"{a}{mul}{result}"
if expr.b != 0:
result = f"{result}+{angle_to_str(rad_to_angle(expr.b), output)}"
return result
[docs]
def command_to_str(cmd: command.Command, output: OutputFormat) -> str:
"""Return the string representation of a command according to the given format.
Parameters
----------
cmd: Command
The command to pretty print.
output: OutputFormat
The expected format.
"""
out = [cmd.kind.name]
match cmd.kind:
case command.CommandKind.E:
u, v = cmd.nodes
match output:
case OutputFormat.LaTeX:
out.append(f"_{{{u},{v}}}")
case OutputFormat.Unicode:
u_subscripts = str(u).translate(SUBSCRIPTS)
v_subscripts = str(v).translate(SUBSCRIPTS)
out.append(f"{u_subscripts}₋{v_subscripts}")
case _:
out.append(f"({u},{v})")
case command.CommandKind.T:
pass
case _:
# All other commands have a field `node` to print, together
# with some other arguments and/or domains.
arguments = []
match cmd.kind:
case command.CommandKind.M:
match cmd.measurement:
case BlochMeasurement(angle, plane):
if plane != Plane.XY:
arguments.append(plane.name)
# We use `SupportsFloat` since `isinstance(cmd.angle, float)`
# is `False` if `cmd.angle` is an integer.
match angle:
case SupportsFloat():
s = angle_to_str(float(angle), output)
case AffineExpression():
s = affine_expression_to_str(angle.scale_non_null(pi), output)
case _:
# If the angle is a symbolic expression, we can only delegate the printing
# TODO: We should have a mean to specify the format
s = str(angle_to_rad(angle))
arguments.append(s)
case PauliMeasurement(Axis.X, Sign.PLUS):
pass
case _:
arguments.append(str(cmd.measurement))
case command.CommandKind.C:
arguments.append(str(cmd.clifford))
match cmd.kind:
case command.CommandKind.X | command.CommandKind.Z | command.CommandKind.S:
command_domain: set[int] | None = cmd.domain
case _:
command_domain = None
match output:
case OutputFormat.LaTeX:
out.append(f"_{{{cmd.node}}}")
if arguments:
out.append(f"^{{{','.join(arguments)}}}")
case OutputFormat.Unicode:
node_subscripts = str(cmd.node).translate(SUBSCRIPTS)
out.append(f"{node_subscripts}")
if arguments:
out.append(f"({','.join(arguments)})")
case _:
arguments = [str(cmd.node), *arguments]
if command_domain:
arguments.append(domain_to_str(command_domain))
command_domain = None
out.append(f"({','.join(arguments)})")
if cmd.kind == command.CommandKind.M and (cmd.s_domain or cmd.t_domain):
out = ["[", *out, "]"]
if cmd.t_domain:
match output:
case OutputFormat.LaTeX:
t_domain_str = f"{{}}_{{{','.join(str(node) for node in cmd.t_domain)}}}"
case OutputFormat.Unicode:
t_domain_subscripts = [str(node).translate(SUBSCRIPTS) for node in cmd.t_domain]
t_domain_str = "₊".join(t_domain_subscripts)
case _:
t_domain_str = f"{{{','.join(str(node) for node in cmd.t_domain)}}}"
out = [t_domain_str, *out]
command_domain = cmd.s_domain
if command_domain:
match output:
case OutputFormat.LaTeX:
domain_str = f"^{{{','.join(str(node) for node in command_domain)}}}"
case OutputFormat.Unicode:
domain_superscripts = [str(node).translate(SUPERSCRIPTS) for node in command_domain]
domain_str = "⁺".join(domain_superscripts)
case _:
domain_str = f"{{{','.join(str(node) for node in command_domain)}}}"
out.append(domain_str)
return f"{''.join(out)}"
[docs]
def pattern_to_str(
pattern: Pattern,
output: OutputFormat,
left_to_right: bool = False,
limit: int | None = 40,
target: Container[command.CommandKind] | None = None,
) -> str:
"""Return the string representation of a pattern according to the given format.
Parameters
----------
pattern: Pattern
The pattern to pretty print.
output: OutputFormat
The expected format.
left_to_right: bool, optional
If ``True``, the first command will appear at the beginning of
the resulting string. If ``False`` (the default), the first command will
appear at the end of the string.
limit: int | None, optional
If set to an int (default: 40), only first ``limit`` commands are printed,
and an ellipsis is added at the end to indicate that some commands have been elided.
If ``limit=None``, there is no limit on the number of printed commands.
target: Container[command.CommandKind], optional
If set, only commands of kinds specified in ``target`` are printed.
"""
separator = r"\," if output == OutputFormat.LaTeX else " "
command_list = list(pattern)
if target is not None:
command_list = [command for command in command_list if command.kind in target]
if not left_to_right:
command_list.reverse()
truncated = limit is not None and len(command_list) > limit
# Note: The redundant test `limit is not None` is required for mypy
# to narrow the type of `limit` in the then-branch.
short_command_list = command_list[: limit - 1] if limit is not None and truncated else command_list
result = separator.join(command_to_str(command, output) for command in short_command_list)
if output == OutputFormat.LaTeX:
result = f"\\({result}\\)"
if limit is not None and truncated:
return f"{result}...({len(command_list) - limit + 1} more commands)"
return result
def set_to_str(objects: Iterable[object], output: OutputFormat) -> str:
"""Convert a set to a formatted string representation.
Parameters
----------
objects : Iterable[object]
The set to format.
output : OutputFormat
The desired output format (ASCII, LaTeX or Unicode).
"""
contents = ", ".join(str(item) for item in objects)
if output == OutputFormat.LaTeX:
return f"\\{{{contents}\\}}"
return f"{{{contents}}}"
def correction_function_to_str(
correction_function: Mapping[int, AbstractSet[int]], cf_name: str, output: OutputFormat, multiline: bool = False
) -> str:
"""Convert a correction function mapping to a formatted string representation.
Parameters
----------
correction_function : Mapping[int, AbstractSet[int]]
A mapping from node indices to sets of node indices representing the
correction function. See :class:`graphix.flow.core.PauliFlow` for additional information.
cf_name : str
The name of the correction function (e.g., ``c`` for causal flow, ``g`` for gflow, etc.)
output : OutputFormat
The desired output format (ASCII, LaTeX or Unicode).
multiline : bool, optional
If ``True``, format each correction set on a separate line (or LaTeX line break).
If ``False``, format each correction set on a single line separated by commas.
Default is ``False``.
Returns
-------
str
"""
separator = (
(r",\\" if output == OutputFormat.LaTeX else "\n")
if multiline
else (r", \;" if output == OutputFormat.LaTeX else ", ")
)
return separator.join(
f"{cf_name}({node}) = {set_to_str(cset, output)}" for node, cset in correction_function.items()
)
def partial_order_to_str(partial_order_layers: Sequence[AbstractSet[int]], output: OutputFormat) -> str:
"""Convert a partial order layering to a formatted string representation.
Parameters
----------
partial_order_layers : Sequence[AbstractSet[int]]
Partial order between nodes in a layer form. See :class:`graphix.flow.core.PauliFlow` for additional information.
output : OutputFormat
The desired output format (ASCII, LaTeX or Unicode).
Returns
-------
str
"""
match output:
case OutputFormat.ASCII:
separator = " < "
case OutputFormat.Unicode:
separator = " ≺ "
case OutputFormat.LaTeX:
separator = r" \prec "
case _:
assert_never(output)
return separator.join(f"{set_to_str(layer, output)}" for layer in partial_order_layers[::-1])
def component_separator_for(output: OutputFormat, multiline: bool = False) -> str:
"""Return a component separator to string-format a `PauliFlow` or a `XZCorrections` object.
Parameters
----------
output : OutputFormat
The desired output format (ASCII, LaTeX or Unicode).
multiline : bool, optional
If ``True``, format each component on a separate line (or LaTeX line break).
If ``False``, format each component set on a single line separated by semicolons.
Default is ``False``.
Returns
-------
str
"""
return (
(r";\\" if output == OutputFormat.LaTeX else "\n")
if multiline
else (r"; \;" if output == OutputFormat.LaTeX else "; ")
)
[docs]
def flow_to_str(flow: PauliFlow[AbstractMeasurement], output: OutputFormat, multiline: bool = False) -> str:
"""Convert a flow object to a formatted string representation.
Parameters
----------
flow : PauliFlow[AbstractMeasurement]
The flow object to be formatted.
output : OutputFormat
The desired output format (ASCII, LaTeX or Unicode).
multiline : bool, optional
If ``True``, format each correction set on a separate line (or LaTeX line break).
If ``False``, format each correction set on a single line separated by commas.
Default is ``False``.
Returns
-------
str
A string representation of the flow object formatted according to the specified output format and layout.
"""
separator = component_separator_for(output, multiline)
return separator.join(
(
correction_function_to_str(flow.correction_function, flow._CF_PREFIX, output, multiline),
partial_order_to_str(flow.partial_order_layers, output),
)
)
[docs]
def xzcorr_to_str(xzcorr: XZCorrections[AbstractMeasurement], output: OutputFormat, multiline: bool = False) -> str:
"""Convert an XZCorrections object to a formatted string representation.
Parameters
----------
flow : XZCorrections[AbstractMeasurement]
The XZCorrections object to be formatted.
output : OutputFormat
The desired output format (ASCII, LaTeX or Unicode).
multiline : bool, optional
If ``True``, format each correction set on a separate line (or LaTeX line break).
If ``False``, format each correction set on a single line separated by commas.
Default is ``False``.
Returns
-------
str
A string representation of the XZCorrections object formatted according to the specified output format and layout.
"""
separator = component_separator_for(output, multiline)
return separator.join(
(
correction_function_to_str(xzcorr.x_corrections, "x", output, multiline),
correction_function_to_str(xzcorr.z_corrections, "z", output, multiline),
partial_order_to_str(xzcorr.partial_order_layers, output),
)
)