Skip to content

Commit ff3d001

Browse files
authored
Format floating-point values in the Rust circuit drawer (#15899)
* Format floating-point values This commit adds formatting functionality to render floating-point values (e.g. rotation angles) in a way similar to the `g` formatting flag in Python. * Address review comments * change input parameter of the formatter to usize * add tests * Integrate Pi-formatting into `F64UiFormatter` * Fix dependency version in `Cargo.toml`
1 parent 4ac5188 commit ff3d001

3 files changed

Lines changed: 196 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/circuit/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ nom-language.workspace = true
3333
crossterm = "0.29.0"
3434
unicode-width = "0.2"
3535
unicode-segmentation = "1.13"
36+
lexical-core = "1.0.6"
37+
lexical-write-float = "1.0.6"
3638

3739
[dependencies.pyo3]
3840
workspace = true

crates/circuit/src/circuit_drawer.rs

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use approx;
1919
use crossterm::terminal;
2020
use hashbrown::HashSet;
2121
use itertools::{Itertools, MinMaxResult};
22+
use lexical_core::ToLexicalWithOptions;
23+
use lexical_write_float::{self, format::STANDARD};
2224
use pyo3::prelude::*;
2325
use std::f64::consts::PI;
2426
use std::fmt::Debug;
@@ -63,7 +65,7 @@ pub fn draw_circuit(
6365
} else {
6466
format!(
6567
"global phase: {}\n",
66-
format_float_pi(*f).unwrap_or_else(|| f.to_string())
68+
F64UiFormatter::new(5).format_with_pi(*f)
6769
)
6870
}
6971
}
@@ -673,6 +675,55 @@ impl TextWireElement {
673675
}
674676
}
675677

678+
/// A formatter for UI rendering of floating-point numbers
679+
///
680+
/// Supports formatting similar to Python's `g` or C printf's `%g` format specifiers
681+
/// as well as formatting of multiples and fractions of pi.
682+
///
683+
/// Example outputs:
684+
/// ```text
685+
/// F64UiFormatter::new(4).format(1.23456) → 1.235
686+
/// F64UiFormatter::new(4).format(123.456) → 123.5
687+
/// F64UiFormatter::new(5).format(12345678.0) → 1.2346e7
688+
/// F64UiFormatter::new(5).format(-0.00001234) → -1.234e-5
689+
/// F64UiFormatter::new(5).format_with_pi(5π/6) → 5π/6
690+
/// ```
691+
struct F64UiFormatter {
692+
buffer: Vec<u8>,
693+
options: lexical_write_float::Options,
694+
}
695+
696+
impl F64UiFormatter {
697+
fn new(num_significant_digits: usize) -> Self {
698+
let options = lexical_write_float::Options::builder()
699+
.max_significant_digits(core::num::NonZeroUsize::new(num_significant_digits))
700+
.positive_exponent_break(core::num::NonZeroI32::new(num_significant_digits as i32))
701+
.negative_exponent_break(core::num::NonZeroI32::new(
702+
-(num_significant_digits as i32) + 1,
703+
))
704+
.trim_floats(true)
705+
.build_strict();
706+
707+
F64UiFormatter {
708+
buffer: vec![0u8; options.buffer_size_const::<f64, STANDARD>()],
709+
options,
710+
}
711+
}
712+
713+
/// Formats the input number based on the formatting options.
714+
/// This Can be called multiple times, but the internal buffer is overwritten on each call.
715+
fn format(&mut self, num: f64) -> &str {
716+
let buf = num.to_lexical_with_options::<STANDARD>(&mut self.buffer, &self.options);
717+
std::str::from_utf8_mut(buf).expect("Byte representation should be valid")
718+
}
719+
720+
/// Tries to format the string as a multiple or simple fraction of pi if possible,
721+
/// otherwise falls back to the simpler [F64UiFormatter::format] logic
722+
fn format_with_pi(&mut self, num: f64) -> String {
723+
format_float_pi(num).unwrap_or_else(|| self.format(num).to_owned())
724+
}
725+
}
726+
676727
pub const Q_WIRE: char = '─';
677728
pub const C_WIRE: char = '═';
678729
pub const TOP_CON: char = '┴';
@@ -735,7 +786,11 @@ impl TextDrawer {
735786
StandardInstruction::Delay(delay_unit) => {
736787
match instruction.params_view().first().unwrap() {
737788
Param::Float(duration) => {
738-
format!("Delay({}[{}])", duration, delay_unit)
789+
format!(
790+
"Delay({}[{}])",
791+
F64UiFormatter::new(5).format(*duration),
792+
delay_unit
793+
)
739794
}
740795
Param::ParameterExpression(expr) => {
741796
format!("Delay({}[{}])", expr, delay_unit)
@@ -767,7 +822,9 @@ impl TextDrawer {
767822
.params_view()
768823
.iter()
769824
.map(|param| match param {
770-
Param::Float(f) => format_float_pi(*f).unwrap_or_else(|| f.to_string()),
825+
Param::Float(f) => {
826+
F64UiFormatter::new(5).format_with_pi(*f).to_string()
827+
}
771828
Param::ParameterExpression(expr) => expr.to_string(),
772829
_ => format!("{:?}", param),
773830
})
@@ -2092,6 +2149,60 @@ q_1: ┤ Ry(🎩) ├┤1 ├┤ 💶🔉(🎩) ├┤1 ├
20922149
assert_eq!(result, expected.trim_start_matches("\n"));
20932150
}
20942151

2152+
#[cfg(not(miri))]
2153+
#[test]
2154+
fn test_f64_formatting() {
2155+
let qubits = vec![
2156+
ShareableQubit::new_anonymous(),
2157+
ShareableQubit::new_anonymous(),
2158+
];
2159+
let mut circuit = CircuitData::new(Some(qubits), None, Param::Float(0.8 * PI)).unwrap();
2160+
2161+
circuit
2162+
.push_standard_gate(StandardGate::RX, &[Param::Float(1.234567)], &[Qubit(0)])
2163+
.unwrap();
2164+
circuit
2165+
.push_standard_gate(StandardGate::RX, &[Param::Float(123.4567)], &[Qubit(0)])
2166+
.unwrap();
2167+
2168+
let expr = ParameterExpression::from_symbol(Symbol::new("ϕ", None, None))
2169+
.mul(&ParameterExpression::from_f64(1.23456))
2170+
.unwrap();
2171+
let param = Param::ParameterExpression(Arc::new(expr));
2172+
circuit
2173+
.push_standard_gate(StandardGate::RY, &[param], &[Qubit(0)])
2174+
.unwrap();
2175+
circuit
2176+
.push_standard_gate(StandardGate::RZ, &[Param::Float(123456789f64)], &[Qubit(1)])
2177+
.unwrap();
2178+
2179+
circuit
2180+
.push_standard_gate(StandardGate::RX, &[Param::Float(0.1234567)], &[Qubit(1)])
2181+
.unwrap();
2182+
circuit
2183+
.push_standard_gate(StandardGate::RX, &[Param::Float(0.0000123456)], &[Qubit(1)])
2184+
.unwrap();
2185+
circuit
2186+
.push_standard_gate(
2187+
StandardGate::RX,
2188+
&[Param::Float(2.0 / 3.0 * PI)],
2189+
&[Qubit(1)],
2190+
)
2191+
.unwrap();
2192+
2193+
let result = draw_circuit(&circuit, true, true, None).unwrap();
2194+
let expected = "
2195+
global phase: 4π/5
2196+
┌────────────┐ ┌────────────┐ ┌───────────────┐
2197+
q_0: ─┤ Rx(1.2346) ├─┤ Rx(123.46) ├─┤ Ry(1.23456*ϕ) ├────────────
2198+
┌┴────────────┴┐├────────────┴┐├───────────────┤┌──────────┐
2199+
q_1: ┤ Rz(1.2346e8) ├┤ Rx(0.12346) ├┤ Rx(1.2346e-5) ├┤ Rx(2π/3) ├
2200+
└──────────────┘└─────────────┘└───────────────┘└──────────┘
2201+
";
2202+
2203+
assert_eq!(result, expected.trim_start_matches("\n"));
2204+
}
2205+
20952206
#[test]
20962207
fn test_format_float_pi() {
20972208
let test_points = [
@@ -2146,4 +2257,25 @@ q_1: ┤ Ry(🎩) ├┤1 ├┤ 💶🔉(🎩) ├┤1 ├
21462257
assert_eq!(format_float_pi(test.0), test.1.map(|s| s.to_string()));
21472258
}
21482259
}
2260+
2261+
#[test]
2262+
fn test_f64_ui_formatter() {
2263+
let test_data_5_sig_digits = [
2264+
(-1.23, "-1.23"),
2265+
(1.23456, "1.2346"),
2266+
(-12.34567, "-12.346"),
2267+
(123456.78, "123460"),
2268+
(-0.0001, "-0.0001"),
2269+
(12.34 * 1_000_000.0, "1.234e7"),
2270+
(-0.00001, "-1e-5"),
2271+
(12345678.000001, "1.2346e7"),
2272+
(15.0 * PI / 16.0, "15π/16"),
2273+
(-2.0 * PI / 3.0, "-2π/3"),
2274+
];
2275+
2276+
let mut formatter = F64UiFormatter::new(5);
2277+
for test in test_data_5_sig_digits {
2278+
assert_eq!(test.1.to_owned(), formatter.format_with_pi(test.0));
2279+
}
2280+
}
21492281
}

0 commit comments

Comments
 (0)