From ebcd7a14dc49730ab5e54819f7baa186e7273d7b Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Mon, 13 Jul 2026 12:23:05 +0200 Subject: [PATCH 1/3] Fix #556: unify default output format This commit unifies the default behavior of display routines by introducing the context variable `default_output_format`, which is set to `OutputFormat.ASCII` by default. The `__str__` methods of `PauliFlow`, `XZCorrections`, `Pattern`, and `StateVec` now directly call the corresponding `*_to_str` functions using the default value for the `output` parameter. This default is resolved by `ensure_output_format`, which retrieves the value of `default_output_format`. Previously, `StateVec` produced output without relying on the pretty-printing functions, while `StateVec.draw` uses `OutputFormat.Unicode` by default and the other classes called `to_ascii` directly. In `docs/source/conf.py`, the following entry has been added to `intersphinx_mapping`: ```python "python": ("https://docs.python.org/3", None), ``` This enables links to the Python documentation for `ContextVar`. The documentation of the `pretty_print` module has been changed to: ``` .. automodule:: graphix.pretty_print :members: ``` so that all public members are included in the documentation. --- CHANGELOG.md | 2 + docs/source/conf.py | 1 + docs/source/visualization.rst | 15 +---- graphix/flow/core.py | 4 +- graphix/pattern.py | 2 +- graphix/pretty_print.py | 103 ++++++++++++++++++++++++++-------- graphix/sim/statevec.py | 4 +- 7 files changed, 89 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f40251f..776aa0426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #545: Added an amplitude damping noise model. Introduces `amplitude_damping_channel` / `two_qubit_amplitude_damping_channel`, the `AmplitudeDampingNoise` / `TwoQubitAmplitudeDampingNoise` noise elements, and `AmplitudeDampingNoiseModel`. +- #556, #563: Added a context variable `default_output_format` to unify the default output format for displaying `PauliFlow`, `XZCorrections`, `Pattern`, and `StateVec`. + ### Fixed - #454, #481: Ensure `Pattern.minimize_space` only reduces max-space and does not increase it. diff --git a/docs/source/conf.py b/docs/source/conf.py index 3f25f64cd..8e8f7f14d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -35,6 +35,7 @@ autosectionlabel_prefix_document = True intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), "networkx": ("https://networkx.github.io/documentation/stable/", None), } diff --git a/docs/source/visualization.rst b/docs/source/visualization.rst index 489838cbd..508542da2 100644 --- a/docs/source/visualization.rst +++ b/docs/source/visualization.rst @@ -17,16 +17,5 @@ If flow or gflow exist, the tool take them into account and show the information This modules provides functions to format patterns and flows. -.. currentmodule:: graphix.pretty_print - -.. autoclass:: OutputFormat - -.. autofunction:: angle_to_str - -.. autofunction:: command_to_str - -.. autofunction:: pattern_to_str - -.. autofunction:: flow_to_str - -.. autofunction:: xzcorr_to_str +.. automodule:: graphix.pretty_print + :members: diff --git a/graphix/flow/core.py b/graphix/flow/core.py index 6d066214d..41d3b0ced 100644 --- a/graphix/flow/core.py +++ b/graphix/flow/core.py @@ -475,7 +475,7 @@ def check_well_formed(self) -> None: def __str__(self) -> str: """Return a human-readable string representing the XZCorrections' mappings and partial order layers.""" - return self.to_ascii() + return xzcorr_to_str(self) def to_ascii(self, multiline: bool = False) -> str: """Return an ASCII string representing the XZCorrections' mappings and partial order layers. @@ -815,7 +815,7 @@ def node_measurement_label(self, node: int) -> Plane | Axis: def __str__(self) -> str: """Return a human-readable string representing the flow's correction function and partial order layers.""" - return self.to_ascii() + return flow_to_str(self) def to_ascii(self, multiline: bool = False) -> str: """Return an ASCII string representing the flow's correction function and partial order layers. diff --git a/graphix/pattern.py b/graphix/pattern.py index ddfebcf3d..9dcee31d6 100644 --- a/graphix/pattern.py +++ b/graphix/pattern.py @@ -446,7 +446,7 @@ def __repr__(self) -> str: def __str__(self) -> str: """Return a human-readable string of the pattern.""" - return self.to_ascii() + return pattern_to_str(self) def __eq__(self, other: object) -> bool: """Return `True` if the two patterns are equal, `False` otherwise.""" diff --git a/graphix/pretty_print.py b/graphix/pretty_print.py index 4303903fe..eac777be1 100644 --- a/graphix/pretty_print.py +++ b/graphix/pretty_print.py @@ -5,6 +5,7 @@ import enum import math import string +from contextvars import ContextVar from enum import Enum from fractions import Fraction from math import pi @@ -38,8 +39,43 @@ class OutputFormat(Enum): Unicode = enum.auto() +default_output_format = ContextVar("default_output_format", default=OutputFormat.ASCII) + + +def ensure_output_format(output: OutputFormat | None) -> OutputFormat: + r"""Return the default output format if ``None`` is given. + + The default output format is stored in the + :class:`~contextvars.ContextVar` ``default_output_format``. + + Parameters + ---------- + output: OutputFormat | None + If not ``None``, this value is returned. + Otherwise, the default output format is returned. + + Examples + -------- + >>> import contextvars + >>> from graphix import Circuit, OutputFormat + >>> from graphix.pretty_print import default_output_format + >>> ctx = contextvars.copy_context() + >>> def main(): + ... circuit = Circuit(1) + ... circuit.h(0) + ... default_output_format.set(OutputFormat.LaTeX) + ... print(circuit.transpile().pattern) + >>> ctx.run(main) + \(X_{1}^{0}\,M_{0}^{0}\,E_{0,1}\,N_{1}\) + + """ + if output is None: + return default_output_format.get() + return output + + def angle_to_str( - angle: Angle, output: OutputFormat, max_denominator: int = 1000, multiplication_sign: bool = False + angle: Angle, output: OutputFormat | None = None, max_denominator: int = 1000, multiplication_sign: bool = False ) -> str: r""" Return a string representation of an angle given in units of π. @@ -52,7 +88,7 @@ def angle_to_str( ---------- angle : float The angle in multiples of π (e.g., 0.5 means π/2). - output : OutputFormat + output : OutputFormat, optional Desired formatting style: Unicode (π symbol), LaTeX (\pi), or ASCII ("pi"). max_denominator : int, optional Maximum denominator for detecting a simple fraction (default: 1000). @@ -69,6 +105,7 @@ def angle_to_str( str The formatted angle. """ + output = ensure_output_format(output) frac = Fraction(angle).limit_denominator(max_denominator) if not math.isclose(angle, float(frac)): @@ -121,8 +158,9 @@ def domain_to_str(domain: set[Node]) -> str: SUPERSCRIPTS = str.maketrans(string.digits, "⁰¹²³⁴⁵⁶⁷⁸⁹") -def affine_expression_to_str(expr: AffineExpression, output: OutputFormat) -> str: +def affine_expression_to_str(expr: AffineExpression, output: OutputFormat | None = None) -> str: """Return the string representation of an affine expression.""" + output = ensure_output_format(output) result = str(expr.x) if expr.a != 1: a = angle_to_str(rad_to_angle(expr.a), output) @@ -139,16 +177,17 @@ def affine_expression_to_str(expr: AffineExpression, output: OutputFormat) -> st return result -def command_to_str(cmd: command.CommandType, output: OutputFormat) -> str: +def command_to_str(cmd: command.CommandType, output: OutputFormat | None = None) -> str: """Return the string representation of a command according to the given format. Parameters ---------- cmd: CommandType The command to pretty print. - output: OutputFormat + output: OutputFormat, optional The expected format. """ + output = ensure_output_format(output) out = [cmd.kind.name] match cmd.kind: @@ -242,7 +281,7 @@ def command_to_str(cmd: command.CommandType, output: OutputFormat) -> str: def pattern_to_str( pattern: Pattern, - output: OutputFormat, + output: OutputFormat | None = None, left_to_right: bool = False, limit: int | None = 40, target: Container[command.CommandKind] | None = None, @@ -253,7 +292,7 @@ def pattern_to_str( ---------- pattern: Pattern The pattern to pretty print. - output: OutputFormat + output: OutputFormat, optional The expected format. left_to_right: bool, optional If ``True``, the first command will appear at the beginning of @@ -266,6 +305,7 @@ def pattern_to_str( target: Container[command.CommandKind], optional If set, only commands of kinds specified in ``target`` are printed. """ + output = ensure_output_format(output) separator = r"\," if output == OutputFormat.LaTeX else " " command_list = list(pattern) if target is not None: @@ -284,16 +324,17 @@ def pattern_to_str( return result -def set_to_str(objects: Iterable[object], output: OutputFormat) -> str: +def set_to_str(objects: Iterable[object], output: OutputFormat | None = None) -> str: """Convert a set to a formatted string representation. Parameters ---------- objects : Iterable[object] The set to format. - output : OutputFormat + output : OutputFormat, optional The desired output format (ASCII, LaTeX or Unicode). """ + output = ensure_output_format(output) contents = ", ".join(str(item) for item in objects) if output == OutputFormat.LaTeX: return f"\\{{{contents}\\}}" @@ -301,7 +342,10 @@ def set_to_str(objects: Iterable[object], output: OutputFormat) -> str: def correction_function_to_str( - correction_function: Mapping[int, AbstractSet[int]], cf_name: str, output: OutputFormat, multiline: bool = False + correction_function: Mapping[int, AbstractSet[int]], + cf_name: str, + output: OutputFormat | None = None, + multiline: bool = False, ) -> str: """Convert a correction function mapping to a formatted string representation. @@ -312,7 +356,7 @@ def correction_function_to_str( 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 + output : OutputFormat, optional 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). @@ -323,6 +367,7 @@ def correction_function_to_str( ------- str """ + output = ensure_output_format(output) separator = ( (r",\\" if output == OutputFormat.LaTeX else "\n") if multiline @@ -334,20 +379,21 @@ def correction_function_to_str( ) -def partial_order_to_str(partial_order_layers: Sequence[AbstractSet[int]], output: OutputFormat) -> str: +def partial_order_to_str(partial_order_layers: Sequence[AbstractSet[int]], output: OutputFormat | None = None) -> 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 + output : OutputFormat, optional The desired output format (ASCII, LaTeX or Unicode). Returns ------- str """ + output = ensure_output_format(output) match output: case OutputFormat.ASCII: separator = " < " @@ -361,7 +407,7 @@ def partial_order_to_str(partial_order_layers: Sequence[AbstractSet[int]], outpu 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: +def _component_separator_for(output: OutputFormat, multiline: bool = False) -> str: """Return a component separator to string-format a `PauliFlow` or a `XZCorrections` object. Parameters @@ -384,14 +430,16 @@ def component_separator_for(output: OutputFormat, multiline: bool = False) -> st ) -def flow_to_str(flow: PauliFlow[AbstractMeasurement], output: OutputFormat, multiline: bool = False) -> str: +def flow_to_str( + flow: PauliFlow[AbstractMeasurement], output: OutputFormat | None = None, 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 + output : OutputFormat, optional 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). @@ -403,7 +451,8 @@ def flow_to_str(flow: PauliFlow[AbstractMeasurement], output: OutputFormat, mult str A string representation of the flow object formatted according to the specified output format and layout. """ - separator = component_separator_for(output, multiline) + output = ensure_output_format(output) + separator = _component_separator_for(output, multiline) return separator.join( ( @@ -413,14 +462,16 @@ def flow_to_str(flow: PauliFlow[AbstractMeasurement], output: OutputFormat, mult ) -def xzcorr_to_str(xzcorr: XZCorrections[AbstractMeasurement], output: OutputFormat, multiline: bool = False) -> str: +def xzcorr_to_str( + xzcorr: XZCorrections[AbstractMeasurement], output: OutputFormat | None = None, 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 + output : OutputFormat, optional 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). @@ -432,7 +483,8 @@ def xzcorr_to_str(xzcorr: XZCorrections[AbstractMeasurement], output: OutputForm str A string representation of the XZCorrections object formatted according to the specified output format and layout. """ - separator = component_separator_for(output, multiline) + output = ensure_output_format(output) + separator = _component_separator_for(output, multiline) return separator.join( ( @@ -669,7 +721,7 @@ def _decimal_to_str(z: complex, output: OutputFormat, precision: int, atol: floa def complex_to_str( value: object, - output: OutputFormat, + output: OutputFormat | None = None, *, max_denominator: int = _DEFAULT_MAX_DENOMINATOR, atol: float = _DEFAULT_ATOL, @@ -690,7 +742,7 @@ def complex_to_str( value : object The number to format. Anything supporting conversion to ``complex`` is accepted; other objects are stringified. - output : OutputFormat + output : OutputFormat, optional Desired formatting style: ``Unicode`` (``√``, ``π``), ``LaTeX`` (``\sqrt``, ``\pi``) or ``ASCII`` (``sqrt``, ``pi``). max_denominator : int, optional @@ -720,6 +772,7 @@ def complex_to_str( >>> complex_to_str(0.123456 + 0.234567j, OutputFormat.ASCII, precision=2) '0.12+0.23i' """ + output = ensure_output_format(output) if not isinstance(value, (bool, int, float, complex, SupportsComplex)): return str(value) z = complex(value) @@ -818,7 +871,7 @@ def _factor_uniform_magnitude( def statevec_to_str( statevec: Statevec, - output: OutputFormat, + output: OutputFormat | None = None, *, encoding: _ENCODING = "MSB", max_denominator: int = _DEFAULT_MAX_DENOMINATOR, @@ -857,6 +910,7 @@ def statevec_to_str( str The formatted statevector, e.g. ``√2/2(|00⟩ + |01⟩)``. """ + output = ensure_output_format(output) amplitudes = statevec.to_dict(encoding, rtol=rtol, atol=atol) if not amplitudes: return "0" @@ -894,7 +948,7 @@ def statevec_to_str( def density_matrix_to_str( density_matrix: DensityMatrix, - output: OutputFormat, + output: OutputFormat | None = None, *, max_denominator: int = _DEFAULT_MAX_DENOMINATOR, atol: float = _DEFAULT_ATOL, @@ -928,6 +982,7 @@ def density_matrix_to_str( str The formatted density matrix. """ + output = ensure_output_format(output) rows = [ [ complex_to_str(entry, output, max_denominator=max_denominator, atol=atol, rtol=rtol, precision=precision) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index cb45e1eb5..03d4a9483 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -145,7 +145,7 @@ def state_to_statevector( def __str__(self) -> str: """Return a string description.""" - return f"Statevec object with statevector {self.psi} and length {self.dims()}." + return self.draw() @override def add_nodes(self, nqubit: int, data: Data) -> None: @@ -531,7 +531,7 @@ def to_prob_dict( def draw( self, - output: OutputFormat = OutputFormat.Unicode, + output: OutputFormat | None = None, *, encoding: _ENCODING = "MSB", max_denominator: int = 1000, From 8ce4dd78f152ab685b72f80b19315c0f00581878 Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Mon, 13 Jul 2026 14:06:15 +0200 Subject: [PATCH 2/3] Fix docstring --- graphix/sim/statevec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py index 03d4a9483..2d90655f3 100644 --- a/graphix/sim/statevec.py +++ b/graphix/sim/statevec.py @@ -575,7 +575,7 @@ def draw( >>> circuit.h(0) >>> circuit.cz(0, 1) >>> print(circuit.simulate_statevector().statevec.draw()) - √2/2(|00⟩ + |01⟩) + sqrt(2)/2(|00> + |01>) """ return statevec_to_str( self, From 43957b80f6279928342ca448925702fdf6ce64fd Mon Sep 17 00:00:00 2001 From: Thierry Martinez Date: Mon, 13 Jul 2026 23:12:23 +0200 Subject: [PATCH 3/3] Fix test coverage --- tests/test_pretty_print.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index da3e47848..2d4559359 100644 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -285,6 +285,7 @@ def test_statevec_draw() -> None: # A magnitude shared by every amplitude is factored out. assert bell.draw(OutputFormat.Unicode) == "√2/2(|00⟩ + |11⟩)" assert bell.draw(OutputFormat.ASCII) == "sqrt(2)/2(|00> + |11>)" + assert str(bell) == bell.draw(OutputFormat.ASCII) # LaTeX uses the \ket{...} macro for the basis kets. assert bell.draw(OutputFormat.LaTeX) == r"\frac{\sqrt{2}}{2}(\ket{00} + \ket{11})"