Pretty-printing for Statevec and DensityMatrix (closes #501)#524
Pretty-printing for Statevec and DensityMatrix (closes #501)#524Vinny010 wants to merge 7 commits into
Conversation
…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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
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
left a comment
There was a problem hiding this comment.
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.
- 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>
|
Thanks for the review, @thierry-martinez! Pushed eebb104 addressing both inline points:
I'll have a look at #503 and #523 and follow up with comments there. |
|
@thierry-martinez — small process note: looks like the workflow run on |
thierry-martinez
left a comment
There was a problem hiding this comment.
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.
|
|
||
| def _ket_str(ket: str, output: OutputFormat) -> str: | ||
| if output == OutputFormat.LaTeX: | ||
| return rf"\lvert {ket}\rangle" |
There was a problem hiding this comment.
I suggest you to use \ket{{ket}} instead.
There was a problem hiding this comment.
The notation has been changed in d3f8a56, but the LaTeX notation for kets is not covered by any test.
| # |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}}" |
There was a problem hiding this comment.
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.
- 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>
thierry-martinez
left a comment
There was a problem hiding this comment.
Thank you for your changes. Just minor comments left. Please have a look at the test coverage: there are still some gaps.
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>
|
Thanks @thierry-martinez — all four addressed in 58eb451.
|
thierry-martinez
left a comment
There was a problem hiding this comment.
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
| encoding: _ENCODING = "MSB", | ||
| max_denominator: int = _DEFAULT_MAX_DENOMINATOR, | ||
| atol: float = _DEFAULT_ATOL, | ||
| rtol: float = 0.0, |
There was a problem hiding this comment.
| rtol: float = 0.0, | |
| rtol: float = _DEFAULT_RTOL, |
| """ | ||
| amplitudes = statevec.to_dict(encoding, rtol=rtol, atol=atol) | ||
| if not amplitudes: | ||
| return "0" |
There was a problem hiding this comment.
This line is not covered by any test.
| if abs(z.imag) <= atol: | ||
| return f"{z.real:.{precision}g}" | ||
| if abs(z.real) <= atol: | ||
| return f"{z.imag:.{precision}g}{unit}" |
There was a problem hiding this comment.
This line is not covered by any tests.
| return None | ||
| radius = _real_to_str(math.hypot(z.real, z.imag), output, max_denominator, atol, rtol) | ||
| if radius is None: | ||
| return None |
There was a problem hiding this comment.
This line is not covered by any tests.
| """Render a purely imaginary value ``x * i``.""" | ||
| rec = _recognize_sqrt(x, max_denominator, atol, rtol) | ||
| if rec is None: | ||
| return None |
There was a problem hiding this comment.
This line is not covered by any tests.
| 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" |
There was a problem hiding this comment.
This line is not covered by any tests.
| Returns ``None`` when ``x`` is not recognized as such a value. | ||
| """ | ||
| if x == 0: | ||
| return 0, 1, 1 |
There was a problem hiding this comment.
This line is not covered by any tests.
| squarefree. ``n == 0`` returns ``(0, 1)``. | ||
| """ | ||
| if n == 0: | ||
| return 0, 1 |
There was a problem hiding this comment.
This line is not covered by any tests.
| >>> circuit.h(0) | ||
| >>> circuit.cz(0, 1) | ||
| >>> print(circuit.simulate_statevector().statevec.draw()) | ||
| √2/2|00⟩ + √2/2|01⟩ |
There was a problem hiding this comment.
Uniform magnitude should be factored out:
| √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>
|
Thanks @thierry-martinez — follow-up addressed in 5b7c389. Uniform magnitude. Statevec output now factors a magnitude shared by every amplitude: Uncovered lines. Added tests reaching each flagged branch:
For the (I checked the new branches execute by tracing the outputs; |
|
Note on the red CI here: the 7 failures are all in Those render graph/flow figures and don't touch anything in this diff, which only changes amplitude/statevector text formatting — nothing in 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. |
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 existingangle_to_strfor the exponential phase. Supports ASCII / Unicode / LaTeX.statevec_to_str+Statevec.draw— ket notation, honouring theto_dictencodingparameter (e.g.√2/2|00⟩ + √2/2|01⟩).density_matrix_to_str+DensityMatrix.draw— column-aligned grid (ASCII/Unicode) or LaTeXpmatrix.Passes
ruff check,ruff format,mypy --strict, andpytestlocally (167 tests in the affected suites).Design points I'd value feedback on:
draw()returns astr, consistent with the existingto_unicode/to_latexstyle.Happy to adjust either.
Developed with LLM assistance, reviewed and tested by me.