quantum-study-utils 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,197 @@
1
+ """
2
+ Formatters for quantum states and numbers in LaTeX notation.
3
+
4
+ Converts quantum objects into mathematically-formatted strings suitable
5
+ for display in Jupyter notebooks and educational contexts.
6
+ """
7
+
8
+ import numpy as np
9
+ from typing import List, Tuple
10
+ from ._utils import (
11
+ as_complex_array,
12
+ validate_qubit_count,
13
+ amplitude_is_near_zero,
14
+ )
15
+
16
+
17
+ class AcademicNumberFormatter:
18
+ """Format numbers using academic/analytic notation."""
19
+
20
+ SQRT2_INV = 1.0 / np.sqrt(2)
21
+ EPSILON = 1e-10
22
+
23
+ @staticmethod
24
+ def format(number: complex, precision: int = 3) -> str:
25
+ """
26
+ Format a real or complex number using analytic notation.
27
+
28
+ Special handling for:
29
+ - ±1/√2: Uses LaTeX fraction notation
30
+ - Integers: No decimal places
31
+ - Pure imaginary: Shows 'i' notation
32
+ - Complex: Shows full form
33
+
34
+ Args:
35
+ number: Number to format (converted to complex)
36
+ precision: Decimal precision for non-special values
37
+
38
+ Returns:
39
+ LaTeX-formatted string representation
40
+
41
+ Example:
42
+ >>> fmt = AcademicNumberFormatter()
43
+ >>> fmt.format(1/np.sqrt(2))
44
+ '\\frac{1}{\\sqrt{2}}'
45
+ >>> fmt.format(1+2j)
46
+ '(1.000+2.000i)'
47
+ """
48
+ number = complex(number)
49
+
50
+ # Handle purely real numbers
51
+ if np.isclose(number.imag, 0, atol=AcademicNumberFormatter.EPSILON):
52
+ real = number.real
53
+
54
+ # Check for ±1/√2
55
+ if np.isclose(abs(real), AcademicNumberFormatter.SQRT2_INV):
56
+ return (
57
+ r"\frac{1}{\sqrt{2}}" if real > 0
58
+ else r"-\frac{1}{\sqrt{2}}"
59
+ )
60
+
61
+ # Check for integers
62
+ if np.isclose(real, round(real)):
63
+ return str(int(round(real)))
64
+
65
+ # Decimal approximation
66
+ return f"{real:.{precision}g}"
67
+
68
+ # Handle purely imaginary numbers
69
+ if np.isclose(number.real, 0, atol=AcademicNumberFormatter.EPSILON):
70
+ return f"{number.imag:.{precision}g}i"
71
+
72
+ # Handle full complex numbers
73
+ sign = "+" if number.imag >= 0 else "-"
74
+ return f"({number.real:.{precision}g}{sign}{abs(number.imag):.{precision}g}i)"
75
+
76
+
77
+ class DiracExpressionGenerator:
78
+ """Convert statevectors into Dirac notation (ket) expressions."""
79
+
80
+ @staticmethod
81
+ def generate(statevector) -> str:
82
+ """
83
+ Convert a statevector into a sum of computational basis kets.
84
+
85
+ Filters out near-zero amplitudes and formats non-zero terms using
86
+ academic number notation. Terms are concatenated with appropriate
87
+ signs.
88
+
89
+ Args:
90
+ statevector: Statevector or Qiskit Statevector object
91
+
92
+ Returns:
93
+ Dirac notation expression as string
94
+
95
+ Raises:
96
+ ValueError: If not a 1D vector
97
+
98
+ Example:
99
+ >>> sv = np.array([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)])
100
+ >>> gen = DiracExpressionGenerator()
101
+ >>> gen.generate(sv)
102
+ '\\frac{1}{\\sqrt{2}}|00\\rangle + \\frac{1}{\\sqrt{2}}|11\\rangle'
103
+ """
104
+ vector = as_complex_array(statevector)
105
+
106
+ if vector.ndim != 1:
107
+ raise ValueError(
108
+ f"Expected 1D statevector, got {vector.ndim}D array"
109
+ )
110
+
111
+ n_qubits = validate_qubit_count(len(vector))
112
+ terms: List[str] = []
113
+ formatter = AcademicNumberFormatter()
114
+
115
+ for index, amplitude in enumerate(vector):
116
+ # Skip near-zero amplitudes
117
+ if amplitude_is_near_zero(amplitude):
118
+ continue
119
+
120
+ # Format basis state
121
+ ket = rf"|{index:0{n_qubits}b}\rangle"
122
+
123
+ # Handle special amplitude cases
124
+ if np.isclose(amplitude, 1):
125
+ term = ket
126
+ elif np.isclose(amplitude, -1):
127
+ term = f"-{ket}"
128
+ else:
129
+ term = f"{formatter.format(amplitude)}{ket}"
130
+
131
+ terms.append(term)
132
+
133
+ if not terms:
134
+ return "0"
135
+
136
+ # Join terms with proper signs
137
+ expression = " + ".join(terms)
138
+ # Replace leading "+ -" with "-"
139
+ expression = expression.replace(" + -", " - ")
140
+
141
+ return expression
142
+
143
+
144
+ class LatexMatrixFormatter:
145
+ """Format matrices in LaTeX matrix notation."""
146
+
147
+ @staticmethod
148
+ def format_vector(vector: np.ndarray) -> str:
149
+ """
150
+ Format a 1D vector in LaTeX matrix notation.
151
+
152
+ Args:
153
+ vector: 1D numpy array
154
+
155
+ Returns:
156
+ LaTeX bmatrix string
157
+ """
158
+ formatter = AcademicNumberFormatter()
159
+ rows = r" \\ ".join(formatter.format(elem) for elem in vector)
160
+ return rf"\begin{{bmatrix}} {rows} \end{{bmatrix}}"
161
+
162
+ @staticmethod
163
+ def format_matrix(matrix: np.ndarray) -> str:
164
+ """
165
+ Format a 2D matrix in LaTeX matrix notation.
166
+
167
+ Args:
168
+ matrix: 2D numpy array
169
+
170
+ Returns:
171
+ LaTeX bmatrix string
172
+ """
173
+ formatter = AcademicNumberFormatter()
174
+ rows = r" \\ ".join(
175
+ " & ".join(formatter.format(elem) for elem in row)
176
+ for row in matrix
177
+ )
178
+ return rf"\begin{{bmatrix}} {rows} \end{{bmatrix}}"
179
+
180
+ @staticmethod
181
+ def format_full_statevector(statevector) -> Tuple[str, str]:
182
+ """
183
+ Format a statevector as both Dirac notation and column vector.
184
+
185
+ Args:
186
+ statevector: Input statevector
187
+
188
+ Returns:
189
+ Tuple of (dirac_expression, matrix_string)
190
+ """
191
+ vector = as_complex_array(statevector)
192
+ validate_qubit_count(len(vector))
193
+
194
+ dirac = DiracExpressionGenerator.generate(vector)
195
+ matrix = LatexMatrixFormatter.format_vector(vector)
196
+
197
+ return dirac, matrix
@@ -0,0 +1,225 @@
1
+ """
2
+ HTML templates for display components.
3
+
4
+ Centralizes HTML markup to improve maintainability and enable theme customization.
5
+ """
6
+
7
+ import html
8
+ from typing import Dict, List
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class HTMLTheme:
14
+ """Configuration for HTML display styling."""
15
+
16
+ # Data display box
17
+ data_border_color: str = "#4A90E2"
18
+ data_padding: str = "12px"
19
+
20
+ # Bar chart styling
21
+ bar_height: str = "13px"
22
+ bar_color: str = "#8866b1"
23
+ bar_bg_color: str = "#e9e9e9"
24
+ bar_border_radius: str = "5px"
25
+
26
+ # Container styling
27
+ container_border_color: str = "#ddd"
28
+ container_padding: str = "14px"
29
+ container_border_radius: str = "7px"
30
+
31
+ # Text styling
32
+ label_font_weight: str = "bold"
33
+ unit_font_size: str = "12px"
34
+ unit_color: str = "#666"
35
+
36
+ # Heatmap sizing
37
+ heatmap_min_size: int = 4
38
+
39
+
40
+ class HTMLBuilder:
41
+ """Helper class for building HTML strings safely with escaping."""
42
+
43
+ @staticmethod
44
+ def safe_text(text: str) -> str:
45
+ """Escape HTML special characters."""
46
+ return html.escape(str(text))
47
+
48
+ @staticmethod
49
+ def div(content: str, style: str = "", **attrs) -> str:
50
+ """Create a div element with optional styling."""
51
+ attr_str = "".join(f' {k}="{HTMLBuilder.safe_text(str(v))}"'
52
+ for k, v in attrs.items() if v is not None)
53
+ style_attr = f' style="{style}"' if style else ""
54
+ return f"<div{style_attr}{attr_str}>{content}</div>"
55
+
56
+ @staticmethod
57
+ def code(text: str) -> str:
58
+ """Create a code element with safe escaping."""
59
+ return f"<code>{HTMLBuilder.safe_text(text)}</code>"
60
+
61
+
62
+ class RawDataTemplate:
63
+ """Template for displaying raw numpy arrays."""
64
+
65
+ @staticmethod
66
+ def render(name: str, array_string: str,
67
+ theme: HTMLTheme = None) -> str:
68
+ """
69
+ Render raw array data with label.
70
+
71
+ Args:
72
+ name: Variable name or label
73
+ array_string: String representation of array (already escaped)
74
+ theme: HTMLTheme configuration
75
+
76
+ Returns:
77
+ HTML string
78
+ """
79
+ if theme is None:
80
+ theme = HTMLTheme()
81
+
82
+ style = (
83
+ f"padding:{theme.data_padding};"
84
+ f"border-left:4px solid {theme.data_border_color};"
85
+ )
86
+
87
+ content = (
88
+ f"<b>{HTMLBuilder.safe_text(name)}</b> "
89
+ f"<code>{array_string}</code>"
90
+ )
91
+
92
+ return HTMLBuilder.div(content, style=style)
93
+
94
+
95
+ class BarChartTemplate:
96
+ """Template for displaying bar charts as HTML."""
97
+
98
+ @staticmethod
99
+ def render_bar(state: str, value: float, width_percent: float,
100
+ theme: HTMLTheme = None) -> str:
101
+ """
102
+ Render a single bar in the chart.
103
+
104
+ Args:
105
+ state: State label (e.g., '|0⟩')
106
+ value: Numeric value to display
107
+ width_percent: Bar width as percentage (0-100)
108
+ theme: HTMLTheme configuration
109
+
110
+ Returns:
111
+ HTML string for single bar
112
+ """
113
+ if theme is None:
114
+ theme = HTMLTheme()
115
+
116
+ width_percent = max(0.0, min(100.0, width_percent))
117
+
118
+ bar_style = (
119
+ f"width:{width_percent:.4f}%;"
120
+ f"height:{theme.bar_height};"
121
+ f"background:{theme.bar_color};"
122
+ )
123
+
124
+ container_style = (
125
+ f"flex:1;"
126
+ f"background:{theme.bar_bg_color};"
127
+ f"border-radius:{theme.bar_border_radius};"
128
+ f"overflow:hidden;"
129
+ )
130
+
131
+ row_style = (
132
+ "display:flex;"
133
+ "align-items:center;"
134
+ "gap:10px;"
135
+ "margin:7px 0;"
136
+ )
137
+
138
+ bar_html = HTMLBuilder.div("", style=bar_style)
139
+ container_html = HTMLBuilder.div(bar_html, style=container_style)
140
+
141
+ value_style = "min-width:95px;text-align:right;"
142
+ value_html = HTMLBuilder.div(
143
+ f"{value:.2f}",
144
+ style=value_style
145
+ )
146
+
147
+ state_html = HTMLBuilder.div(
148
+ HTMLBuilder.code(f"|{state}⟩"),
149
+ style="width:55px;"
150
+ )
151
+
152
+ row_content = state_html + container_html + value_html
153
+
154
+ return HTMLBuilder.div(row_content, style=row_style)
155
+
156
+ @staticmethod
157
+ def render_chart(data: Dict[str, float], label: str,
158
+ suffix: str = "", unit: str = "",
159
+ theme: HTMLTheme = None) -> str:
160
+ """
161
+ Render complete bar chart.
162
+
163
+ Args:
164
+ data: Dict mapping state labels to values
165
+ label: Chart title/label
166
+ suffix: Suffix for values (e.g., '%')
167
+ unit: Unit description
168
+ theme: HTMLTheme configuration
169
+
170
+ Returns:
171
+ HTML string for complete chart
172
+ """
173
+ if theme is None:
174
+ theme = HTMLTheme()
175
+
176
+ bars = []
177
+ for state in sorted(data.keys()):
178
+ value = data[state]
179
+ # Display value with suffix
180
+ display_value = value if not suffix else value # suffix shown separately
181
+ bar_width = max(0.0, min(100.0, value))
182
+
183
+ bar_html = BarChartTemplate.render_bar(
184
+ state, display_value, bar_width, theme
185
+ )
186
+ bars.append(bar_html)
187
+
188
+ container_style = (
189
+ f"max-width:550px;"
190
+ f"padding:{theme.container_padding};"
191
+ f"border:1px solid {theme.container_border_color};"
192
+ f"border-radius:{theme.container_border_radius};"
193
+ )
194
+
195
+ label_html = f"<b>{HTMLBuilder.safe_text(label)}</b>"
196
+ unit_html = (
197
+ f'<div style="font-size:{theme.unit_font_size};'
198
+ f'color:{theme.unit_color};">'
199
+ f"{HTMLBuilder.safe_text(unit)}</div>"
200
+ if unit else ""
201
+ )
202
+
203
+ bars_html = "".join(bars)
204
+ content = label_html + unit_html + bars_html
205
+
206
+ return HTMLBuilder.div(content, style=container_style)
207
+
208
+
209
+ class NormalizationNoticeTemplate:
210
+ """Template for normalization status notice."""
211
+
212
+ @staticmethod
213
+ def render(norm_squared: float) -> str:
214
+ """
215
+ Render normalization status.
216
+
217
+ Args:
218
+ norm_squared: ⟨ψ|ψ⟩ value
219
+
220
+ Returns:
221
+ HTML string
222
+ """
223
+ return (
224
+ f'<small>Normalized state: ⟨ψ|ψ⟩ = {norm_squared:.4f}</small>'
225
+ )
@@ -0,0 +1,136 @@
1
+
2
+ """
3
+ Formatting utilities for quantum states and measurement results.
4
+
5
+ Provides text-based representations for quantum statevectors
6
+ and measurement counts.
7
+
8
+ Compatible with NumPy, Qiskit Statevector and standard Python.
9
+ """
10
+
11
+ from collections.abc import Mapping
12
+ from typing import Sequence
13
+
14
+ import numpy as np
15
+
16
+
17
+ def format_statevector(
18
+ statevector: Sequence[complex],
19
+ precision: int = 4,
20
+ threshold: float = 1e-10,
21
+ ) -> str:
22
+ """
23
+ Format a quantum statevector using computational basis notation.
24
+
25
+ Args:
26
+ statevector:
27
+ Sequence of complex amplitudes or Qiskit Statevector.
28
+ precision:
29
+ Number of decimal places.
30
+ threshold:
31
+ Minimum amplitude magnitude to display.
32
+
33
+ Returns:
34
+ Formatted string representation.
35
+ """
36
+
37
+ if hasattr(statevector, "data"):
38
+ statevector = statevector.data
39
+
40
+ amplitudes = np.asarray(statevector, dtype=complex)
41
+
42
+ if amplitudes.ndim != 1:
43
+ raise ValueError("Statevector must be one-dimensional.")
44
+
45
+ dimension = len(amplitudes)
46
+
47
+ if dimension == 0 or dimension & (dimension - 1):
48
+ raise ValueError(
49
+ "Statevector length must be a power of two."
50
+ )
51
+
52
+ if precision < 0:
53
+ raise ValueError("Precision must be non-negative.")
54
+
55
+ if threshold < 0:
56
+ raise ValueError("Threshold must be non-negative.")
57
+
58
+ num_qubits = max(1, dimension.bit_length() - 1)
59
+
60
+ results = []
61
+
62
+ for index, amplitude in enumerate(amplitudes):
63
+
64
+ if abs(amplitude) <= threshold:
65
+ continue
66
+
67
+ bitstring = format(index, f"0{num_qubits}b")
68
+
69
+ real = float(amplitude.real)
70
+ imag = float(amplitude.imag)
71
+
72
+ if abs(imag) <= threshold:
73
+ formatted = f"{real:.{precision}f}"
74
+
75
+ elif abs(real) <= threshold:
76
+ formatted = f"{imag:.{precision}f}j"
77
+
78
+ else:
79
+ sign = "+" if imag >= 0 else "-"
80
+ formatted = (
81
+ f"{real:.{precision}f}"
82
+ f"{sign}"
83
+ f"{abs(imag):.{precision}f}j"
84
+ )
85
+
86
+ results.append(
87
+ f"|{bitstring}⟩: {formatted}"
88
+ )
89
+
90
+ return "\n".join(results) if results else "Zero statevector"
91
+
92
+
93
+ def format_measurement_results(
94
+ counts: Mapping[str, int],
95
+ precision: int = 2,
96
+ ) -> str:
97
+ """
98
+ Format quantum measurement counts and percentages.
99
+
100
+ Args:
101
+ counts:
102
+ Mapping of measured bitstrings to shot counts.
103
+ precision:
104
+ Decimal places for percentages.
105
+
106
+ Returns:
107
+ Formatted measurement summary.
108
+ """
109
+
110
+ if not counts:
111
+ return "No measurements available."
112
+
113
+ if any(count < 0 for count in counts.values()):
114
+ raise ValueError(
115
+ "Measurement counts cannot be negative."
116
+ )
117
+
118
+ total_shots = sum(counts.values())
119
+
120
+ if total_shots == 0:
121
+ return "No measurement shots recorded."
122
+
123
+ results = []
124
+
125
+ for bitstring in sorted(counts):
126
+
127
+ count = counts[bitstring]
128
+ percentage = 100 * count / total_shots
129
+
130
+ results.append(
131
+ f"|{bitstring}⟩: "
132
+ f"{count} "
133
+ f"({percentage:.{precision}f}%)"
134
+ )
135
+
136
+ return "\n".join(results)