Skip to content

Pretty-printing for Statevec and DensityMatrix (closes #501)#524

Open
Vinny010 wants to merge 7 commits into
TeamGraphix:masterfrom
Vinny010:add-statevec-density-pretty-print
Open

Pretty-printing for Statevec and DensityMatrix (closes #501)#524
Vinny010 wants to merge 7 commits into
TeamGraphix:masterfrom
Vinny010:add-statevec-density-pretty-print

Conversation

@Vinny010

@Vinny010 Vinny010 commented Jun 3, 2026

Copy link
Copy Markdown

Closes #501.

Adds human-friendly rendering of amplitudes and states:

  • complex_to_str — renders common values exactly (1/4, √2/2, e^(iπ/3)) via a square-then-rationalize heuristic, reusing the existing angle_to_str for the exponential phase. Supports ASCII / Unicode / LaTeX.
  • statevec_to_str + Statevec.draw — ket notation, honouring the to_dict encoding parameter (e.g. √2/2|00⟩ + √2/2|01⟩).
  • density_matrix_to_str + DensityMatrix.draw — column-aligned grid (ASCII/Unicode) or LaTeX pmatrix.
  • Tests covering the issue examples plus edge cases (zero, negative, pure imaginary, LaTeX, decimal fallback, symbolic), with numpy-style docstrings.

Passes ruff check, ruff format, mypy --strict, and pytest locally (167 tests in the affected suites).

Design points I'd value feedback on:

  • draw() returns a str, consistent with the existing to_unicode / to_latex style.
  • The complex formatter prefers exponential form over cartesian when both magnitude and phase are recognized.

Happy to adjust either.

Developed with LLM assistance, reviewed and tested by me.

…x#501)

Add human-friendly rendering of quantum amplitudes and states:

- `complex_to_str` in `pretty_print.py` recognizes common values and
  renders them exactly instead of as floats: fractions (`1/4`), square
  roots (`√2/2`) and complex exponentials (`e^(iπ/3)`). Recognition uses a
  square-then-rationalize heuristic and reuses the existing `angle_to_str`
  for the exponential phase. Supports ASCII, Unicode and LaTeX output.
- `statevec_to_str` + `Statevec.draw` render a statevector in ket notation
  (e.g. `√2/2|00⟩ + √2/2|01⟩`), honouring the existing `encoding` parameter
  from `Statevec.to_dict`.
- `density_matrix_to_str` + `DensityMatrix.draw` render a density matrix as
  a column-aligned grid (ASCII/Unicode) or a LaTeX `pmatrix`.
- Tests covering the issue examples plus edge cases (zero, negative, pure
  imaginary, LaTeX, decimal fallback, symbolic), with numpy-style docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.51691% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.15%. Comparing base (fcdb8f4) to head (5b7c389).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
graphix/pretty_print.py 99.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #524      +/-   ##
==========================================
+ Coverage   88.85%   89.15%   +0.29%     
==========================================
  Files          49       49              
  Lines        7135     7341     +206     
==========================================
+ Hits         6340     6545     +205     
- Misses        795      796       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Vinny010 and others added 2 commits June 4, 2026 10:12
Cover the exponential-with-radius, cartesian, complex-decimal-fallback and
LaTeX/ASCII imaginary branches of complex_to_str, the integer-times-root render
path, and the negative-term, parenthesized-coefficient and unit-negative
branches of Statevec.draw, addressing the patch-coverage gap reported on the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build the complex-amplitude statevec from a numpy array (numpy.complex128) rather
than a Python complex literal: Python's complex only gained __complex__ in 3.11,
so a bare complex is not a typing.SupportsComplex on 3.10 and is rejected by
Statevec. Also use a real amplitude for the unit-negative case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@thierry-martinez thierry-martinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for your contribution!

Codecov has detected that some parts of your code are not tested.

As you may have noticed, PR #503 and PR #523 address the same issue (#501). We invite you to cross‑review each other's work: PR reviews are an inherent part of software development, just as important as writing code. Moreover, this review process will help you position your PR relative to the others, allowing us to determine which contribution best addresses the issue.

Comment thread graphix/pretty_print.py Outdated
Comment thread graphix/pretty_print.py Outdated
- Add a ``precision`` keyword argument to :func:`complex_to_str`,
  :func:`statevec_to_str`, :func:`density_matrix_to_str` and the matching
  :meth:`Statevec.draw` / :meth:`DensityMatrix.draw` methods to control the
  number of significant digits of the decimal fallback. The default value
  (``4``) preserves the previous behaviour, and a regression test exercises
  multiple precisions.
- Rename ``_recognize_real`` to ``_recognize_sqrt`` to better reflect the
  ``signed_num * sqrt(inner) / den`` form the helper returns (pure rationals
  are covered as ``inner == 1``); update the docstring accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Vinny010

Vinny010 commented Jun 5, 2026

Copy link
Copy Markdown
Author

Thanks for the review, @thierry-martinez! Pushed eebb104 addressing both inline points:

  • Configurable precision — added a precision keyword argument to complex_to_str, statevec_to_str, density_matrix_to_str, and the matching Statevec.draw / DensityMatrix.draw methods. It controls the number of significant digits used by the decimal fallback. The default (4) preserves the previous behaviour. New regression test covering several precisions.
  • Rename — renamed _recognize_real to _recognize_sqrt. The helper recognizes a real number as signed_num * sqrt(inner) / den, and the rationals are covered as the special case inner == 1 — added a sentence to the docstring to make that explicit.

I'll have a look at #503 and #523 and follow up with comments there.

@Vinny010

Vinny010 commented Jun 6, 2026

Copy link
Copy Markdown
Author

@thierry-martinez — small process note: looks like the workflow run on eebb104 is still awaiting approval (no check runs have fired since the response push). Whenever you have a moment to re-trigger it, happy to address anything else that comes up.

@thierry-martinez thierry-martinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your update, @Vinny010. I think we are on a good track to merge this PR. In this respect, please add a comment in the discussion of #501 so that I can assign the issue to you.

I have also made a few remarks (as a side note, I did not use the "Request changes" flag since we've chosen not to use it in the Graphix project, to avoid blocking the PR from being merged if other maintainers approve it). Note that we have a two‑approval rule for non‑trivial PRs: therefore, once I or another maintainer approves this PR, we'll need a second approval before merging.

Comment thread graphix/pretty_print.py Outdated

def _ket_str(ket: str, output: OutputFormat) -> str:
if output == OutputFormat.LaTeX:
return rf"\lvert {ket}\rangle"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you to use \ket{{ket}} instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notation has been changed in d3f8a56, but the LaTeX notation for kets is not covered by any test.

Comment thread tests/test_pretty_print.py Outdated
Comment thread tests/test_pretty_print.py Outdated
Comment thread tests/test_pretty_print.py Outdated
Comment thread tests/test_pretty_print.py Outdated
# |z| != 1: the radius prefixes the exponential form (1 + i = √2 e^{iπ/4}).
assert complex_to_str(1 + 1j, OutputFormat.Unicode) == "√2·e^(iπ/4)"
assert complex_to_str(1 + 1j, OutputFormat.ASCII) == "sqrt(2)*e^(i*pi/4)"
assert complex_to_str(1 + 1j, OutputFormat.LaTeX) == r"\sqrt{2} \mathrm{e}^{\mathrm{i} \frac{\pi}{4}}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure √2·e^(iπ/4) is any simpler to read than 1 + i. More generally, I tend to think that rational multiples of √2·e^(±iπ/4) are more readable when written as Cartesian form.

Comment thread tests/test_pretty_print.py Outdated
- Use \ket{} in LaTeX kets instead of \lvert ... \rangle
- Render pure imaginary values with the unit leading the numerator
  (i/2, -i√2/2) via a dedicated _render_imaginary helper
- Prefer the Cartesian form over a radius-prefixed exponential when
  |z| != 1 (1 + i instead of √2·e^(iπ/4)); keep the radius-prefixed
  exponential only as a last resort before the decimal fallback
- Merge the format-specific complex_to_str tests into a single
  parametrized test keyed by a Mapping[OutputFormat, str]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pranav97nair pranav97nair self-requested a review June 10, 2026 15:21

@thierry-martinez thierry-martinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your changes. Just minor comments left. Please have a look at the test coverage: there are still some gaps.

Comment thread tests/test_pretty_print.py Outdated
Comment thread graphix/sim/density_matrix.py
Comment thread graphix/pretty_print.py Outdated
Responds to @thierry-martinez's review on TeamGraphix#524:

- Add an `rtol` parameter everywhere an `atol` is exposed: `complex_to_str`,
  `density_matrix_to_str`/`DensityMatrix.draw` (it was missing), and threaded
  through the recognition helpers into the `math.isclose` calls. The default
  (0.0) preserves the previous behaviour.
- `_decimal_to_str` now takes `atol` as an argument instead of relying on the
  module-level `_DEFAULT_ATOL`.
- Test coverage:
  - Merge the remaining `complex_to_str` value tests (radius/Cartesian/decimal
    fallback/integer-times-surd) into the parametrized `test_complex_to_str_values`.
  - Cover the LaTeX `\ket{...}` notation in the statevec draw tests (was untested).
  - Add tests exercising `rtol` (recognition control + `DensityMatrix.draw`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Vinny010

Copy link
Copy Markdown
Author

Thanks @thierry-martinez — all four addressed in 58eb451.

  • rtol parameter. Added wherever atol is exposed: complex_to_str, density_matrix_to_str and DensityMatrix.draw (it was indeed missing there), threaded down into the math.isclose recognition calls as rel_tol. Statevec.draw/statevec_to_str already had it; it now also reaches the per-amplitude recognition, not just the amplitude-dropping. Default is 0.0, so existing behaviour is unchanged.
  • _decimal_to_str. Now takes atol as an argument instead of reading the module-level _DEFAULT_ATOL.
  • LaTeX ket coverage. The \ket{...} notation is now exercised in the statevec draw tests (both the bare basis state and the coefficient-prefixed Bell case).
  • Test consolidation. Merged the remaining complex_to_str value tests (radius/Cartesian/decimal-fallback/integer-times-surd) into the parametrized test_complex_to_str_values. I kept two things out of it deliberately: test_complex_to_str_precision_is_configurable (it varies the precision argument rather than testing a default-args value→string mapping) and the Statevec/DensityMatrix draw tests (different entry points). Happy to fold those in too if you'd rather have a single table.
  • Coverage for rtol. Added test_complex_to_str_rtol_controls_recognition (a value just off 1/2 is a decimal at the default tolerance, but recognized as 1/2 with a looser rtol) and a DensityMatrix.draw(rtol=...) test.

@thierry-martinez thierry-martinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted the coverage results from Codecov. You can view them here: https://app.codecov.io/gh/TeamGraphix/graphix/pull/524?src=pr&el=tree

Comment thread graphix/pretty_print.py
encoding: _ENCODING = "MSB",
max_denominator: int = _DEFAULT_MAX_DENOMINATOR,
atol: float = _DEFAULT_ATOL,
rtol: float = 0.0,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rtol: float = 0.0,
rtol: float = _DEFAULT_RTOL,

Comment thread graphix/pretty_print.py
"""
amplitudes = statevec.to_dict(encoding, rtol=rtol, atol=atol)
if not amplitudes:
return "0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not covered by any test.

Comment thread graphix/pretty_print.py
if abs(z.imag) <= atol:
return f"{z.real:.{precision}g}"
if abs(z.real) <= atol:
return f"{z.imag:.{precision}g}{unit}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not covered by any tests.

Comment thread graphix/pretty_print.py
return None
radius = _real_to_str(math.hypot(z.real, z.imag), output, max_denominator, atol, rtol)
if radius is None:
return None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not covered by any tests.

Comment thread graphix/pretty_print.py
"""Render a purely imaginary value ``x * i``."""
rec = _recognize_sqrt(x, max_denominator, atol, rtol)
if rec is None:
return None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not covered by any tests.

Comment thread graphix/pretty_print.py
def _render_real(signed_num: int, inner: int, den: int, output: OutputFormat) -> str:
"""Render ``signed_num * sqrt(inner) / den`` produced by :func:`_recognize_sqrt`."""
if signed_num == 0:
return "0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not covered by any tests.

Comment thread graphix/pretty_print.py Outdated
Returns ``None`` when ``x`` is not recognized as such a value.
"""
if x == 0:
return 0, 1, 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not covered by any tests.

Comment thread graphix/pretty_print.py
squarefree. ``n == 0`` returns ``(0, 1)``.
"""
if n == 0:
return 0, 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not covered by any tests.

Comment thread graphix/sim/statevec.py Outdated
>>> circuit.h(0)
>>> circuit.cz(0, 1)
>>> print(circuit.simulate_statevector().statevec.draw())
√2/2|00⟩ + √2/2|01⟩

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uniform magnitude should be factored out:

Suggested change
2/2|00+ 2/2|01
2/2(|00+ |01)

Responds to @thierry-martinez's follow-up review on TeamGraphix#524:

- Factor a magnitude shared by every amplitude in the ket-notation output, so a
  state prints as `√2/2(|00⟩ + |11⟩)` rather than `√2/2|00⟩ + √2/2|11⟩` (signs
  are kept inside the parentheses). Compound (Cartesian) and non-uniform
  amplitudes fall back to the previous term-by-term rendering. Updated the
  affected tests and the docstring / doctest examples.
- Cover the branches flagged as untested: the tiny-real -> "0" path (square-free
  decomposition and real-renderer zero branches), the decimal imaginary fallback,
  the exponential form with an unrecognized radius, and the empty-statevector "0".
- Drop the redundant `x == 0` guard in `_recognize_sqrt`: the general path already
  returns `(0, 1, 1)` for `x == 0`, so the early return was dead code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Vinny010

Copy link
Copy Markdown
Author

Thanks @thierry-martinez — follow-up addressed in 5b7c389.

Uniform magnitude. Statevec output now factors a magnitude shared by every amplitude: √2/2(|00⟩ + |11⟩) instead of √2/2|00⟩ + √2/2|11⟩, with the signs kept inside the parentheses (1/2(|00⟩ - |01⟩ + |10⟩ + |11⟩)). Compound (Cartesian) amplitudes and non-uniform magnitudes fall back to the term-by-term form. Updated the affected tests and the docstring / doctest examples.

Uncovered lines. Added tests reaching each flagged branch:

  • tiny non-zero real → "0" (the zero branches of _squarefree_decomposition and _render_real),
  • a purely imaginary decimal (0.234567j0.2346i, the _imaginary_to_str → None and _decimal_to_str imaginary branches),
  • a recognized phase with an unrecognized radius (π·e^(iπ/4), the _exponential_to_str radius-None branch),
  • the empty-statevector "0" (via a tolerance large enough to drop every amplitude).

For the _recognize_sqrt x == 0 guard I removed it rather than test it: the general path already returns (0, 1, 1) for x == 0, so it was dead code.

(I checked the new branches execute by tracing the outputs; pytest-cov isn't in my local env, so if your coverage run still flags anything I missed, point me at the line and I'll cover it.)

@Vinny010

Copy link
Copy Markdown
Author

Note on the red CI here: the 7 failures are all in tests/test_visualization.py — the pytest-mpl image-comparison tests (test_og_draw, the test_*_flow_draw set, test_xzcorr_draw, test_draw_graph_reference), each failing with "Image files did not match". The other 3342 tests pass.

Those render graph/flow figures and don't touch anything in this diff, which only changes amplitude/statevector text formatting — nothing in visualization.py calls complex_to_str/statevec_to_str/density_matrix_to_str. The same --mpl suite passed on #526's CI run a day earlier (11 Jun), so this looks like matplotlib-version pixel drift in the reference images rather than a regression from this branch.

Happy to help if a baseline refresh is wanted — if you let me know the matplotlib version the CI pins to, I can regenerate the references against it; or if you'd rather keep baseline changes out of this PR, that's fine too and I'll leave it to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pretty-print for Statevec and DensityMatrix

2 participants