Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/circuit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ nom-language.workspace = true
crossterm = "0.29.0"
unicode-width = "0.2"
unicode-segmentation = "1.13"
lexical-core = "1.0.6"
lexical-write-float = "1.0.6"

[dependencies.pyo3]
workspace = true
Expand Down
138 changes: 135 additions & 3 deletions crates/circuit/src/circuit_drawer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use approx;
use crossterm::terminal;
use hashbrown::HashSet;
use itertools::{Itertools, MinMaxResult};
use lexical_core::ToLexicalWithOptions;
use lexical_write_float::{self, format::STANDARD};
use pyo3::prelude::*;
use std::f64::consts::PI;
use std::fmt::Debug;
Expand Down Expand Up @@ -63,7 +65,7 @@ pub fn draw_circuit(
} else {
format!(
"global phase: {}\n",
format_float_pi(*f).unwrap_or_else(|| f.to_string())
F64UiFormatter::new(5).format_with_pi(*f)
)
}
}
Expand Down Expand Up @@ -673,6 +675,55 @@ impl TextWireElement {
}
}

/// A formatter for UI rendering of floating-point numbers
///
/// Supports formatting similar to Python's `g` or C printf's `%g` format specifiers
/// as well as formatting of multiples and fractions of pi.
///
/// Example outputs:
/// ```text
/// F64UiFormatter::new(4).format(1.23456) → 1.235
/// F64UiFormatter::new(4).format(123.456) → 123.5
/// F64UiFormatter::new(5).format(12345678.0) → 1.2346e7
/// F64UiFormatter::new(5).format(-0.00001234) → -1.234e-5
/// F64UiFormatter::new(5).format_with_pi(5π/6) → 5π/6
/// ```
struct F64UiFormatter {
buffer: Vec<u8>,
options: lexical_write_float::Options,
}

impl F64UiFormatter {
fn new(num_significant_digits: usize) -> Self {
let options = lexical_write_float::Options::builder()
.max_significant_digits(core::num::NonZeroUsize::new(num_significant_digits))
.positive_exponent_break(core::num::NonZeroI32::new(num_significant_digits as i32))
.negative_exponent_break(core::num::NonZeroI32::new(
-(num_significant_digits as i32) + 1,
))
.trim_floats(true)
.build_strict();

F64UiFormatter {
buffer: vec![0u8; options.buffer_size_const::<f64, STANDARD>()],
Comment thread
Cryoris marked this conversation as resolved.
options,
}
}

/// Formats the input number based on the formatting options.
/// This Can be called multiple times, but the internal buffer is overwritten on each call.
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.

Are we testing anywhere that this can be called multiple times? Looking at the code it seems that a new formatter is instantiated every time when we call .format.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

While I did call one formatter multiple times while I was testing this manually, I thought that since the crate is built around the notion of using a static buffer anyway, it's not worth to add explicit tests for that and that we can just go with the other circuit output tests which for the formatting aspect itself.
That said, I've added such test anyway in d9c2307 since a) it woudln't hurt and b) I want to extend it after #15917 is merged to have a test which covers both $\pi$-based formatting and decimal formatting.

fn format(&mut self, num: f64) -> &str {
let buf = num.to_lexical_with_options::<STANDARD>(&mut self.buffer, &self.options);
std::str::from_utf8_mut(buf).expect("Byte representation should be valid")
}

/// Tries to format the string as a multiple or simple fraction of pi if possible,
/// otherwise falls back to the simpler [F64UiFormatter::format] logic
fn format_with_pi(&mut self, num: f64) -> String {
format_float_pi(num).unwrap_or_else(|| self.format(num).to_owned())
}
}

pub const Q_WIRE: char = '─';
pub const C_WIRE: char = '═';
pub const TOP_CON: char = '┴';
Expand Down Expand Up @@ -735,7 +786,11 @@ impl TextDrawer {
StandardInstruction::Delay(delay_unit) => {
match instruction.params_view().first().unwrap() {
Param::Float(duration) => {
format!("Delay({}[{}])", duration, delay_unit)
format!(
"Delay({}[{}])",
F64UiFormatter::new(5).format(*duration),
delay_unit
)
}
Param::ParameterExpression(expr) => {
format!("Delay({}[{}])", expr, delay_unit)
Expand Down Expand Up @@ -767,7 +822,9 @@ impl TextDrawer {
.params_view()
.iter()
.map(|param| match param {
Param::Float(f) => format_float_pi(*f).unwrap_or_else(|| f.to_string()),
Param::Float(f) => {
F64UiFormatter::new(5).format_with_pi(*f).to_string()
}
Param::ParameterExpression(expr) => expr.to_string(),
_ => format!("{:?}", param),
})
Expand Down Expand Up @@ -2092,6 +2149,60 @@ q_1: ┤ Ry(🎩) ├┤1 ├┤ 💶🔉(🎩) ├┤1 ├
assert_eq!(result, expected.trim_start_matches("\n"));
}

#[cfg(not(miri))]
Comment thread
Cryoris marked this conversation as resolved.
#[test]
fn test_f64_formatting() {
let qubits = vec![
ShareableQubit::new_anonymous(),
ShareableQubit::new_anonymous(),
];
let mut circuit = CircuitData::new(Some(qubits), None, Param::Float(0.8 * PI)).unwrap();

circuit
.push_standard_gate(StandardGate::RX, &[Param::Float(1.234567)], &[Qubit(0)])
.unwrap();
circuit
.push_standard_gate(StandardGate::RX, &[Param::Float(123.4567)], &[Qubit(0)])
.unwrap();

let expr = ParameterExpression::from_symbol(Symbol::new("ϕ", None, None))
.mul(&ParameterExpression::from_f64(1.23456))
.unwrap();
let param = Param::ParameterExpression(Arc::new(expr));
circuit
.push_standard_gate(StandardGate::RY, &[param], &[Qubit(0)])
.unwrap();
circuit
.push_standard_gate(StandardGate::RZ, &[Param::Float(123456789f64)], &[Qubit(1)])
.unwrap();

circuit
.push_standard_gate(StandardGate::RX, &[Param::Float(0.1234567)], &[Qubit(1)])
.unwrap();
circuit
.push_standard_gate(StandardGate::RX, &[Param::Float(0.0000123456)], &[Qubit(1)])
.unwrap();
circuit
.push_standard_gate(
StandardGate::RX,
&[Param::Float(2.0 / 3.0 * PI)],
&[Qubit(1)],
)
.unwrap();

let result = draw_circuit(&circuit, true, true, None).unwrap();
let expected = "
global phase: 4π/5
┌────────────┐ ┌────────────┐ ┌───────────────┐
q_0: ─┤ Rx(1.2346) ├─┤ Rx(123.46) ├─┤ Ry(1.23456*ϕ) ├────────────
┌┴────────────┴┐├────────────┴┐├───────────────┤┌──────────┐
q_1: ┤ Rz(1.2346e8) ├┤ Rx(0.12346) ├┤ Rx(1.2346e-5) ├┤ Rx(2π/3) ├
└──────────────┘└─────────────┘└───────────────┘└──────────┘
";

assert_eq!(result, expected.trim_start_matches("\n"));
}

#[test]
fn test_format_float_pi() {
let test_points = [
Expand Down Expand Up @@ -2146,4 +2257,25 @@ q_1: ┤ Ry(🎩) ├┤1 ├┤ 💶🔉(🎩) ├┤1 ├
assert_eq!(format_float_pi(test.0), test.1.map(|s| s.to_string()));
}
}

#[test]
fn test_f64_ui_formatter() {
let test_data_5_sig_digits = [
(-1.23, "-1.23"),
(1.23456, "1.2346"),
(-12.34567, "-12.346"),
(123456.78, "123460"),
(-0.0001, "-0.0001"),
(12.34 * 1_000_000.0, "1.234e7"),
(-0.00001, "-1e-5"),
(12345678.000001, "1.2346e7"),
(15.0 * PI / 16.0, "15π/16"),
(-2.0 * PI / 3.0, "-2π/3"),
];

let mut formatter = F64UiFormatter::new(5);
for test in test_data_5_sig_digits {
assert_eq!(test.1.to_owned(), formatter.format_with_pi(test.0));
}
}
}