Skip to main content

cqlib_core/visualization/circuit/
params.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! Parameter label formatting policies for visualization IR.
14//!
15//! This module decouples parameter display from the IR build pipeline. Builder code
16//! collects parameters and delegates formatting to [`ParameterFormatter`].
17
18use crate::circuit::Circuit;
19use crate::circuit::circuit_param::CircuitParam;
20use crate::circuit::parameter::Parameter;
21use crate::visualization::VisualizationError;
22use std::f64::consts::PI;
23
24/// Display mode used by the parameter formatter.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ParameterDisplayMode {
27    /// Prefer numeric display when value is evaluable.
28    Numeric,
29    /// Prefer symbolic expression display.
30    Symbolic,
31    /// Show symbolic expression and append numeric value when evaluable.
32    SymbolicWithValue,
33    /// Prefer `kπ/n` representation for values close to common angle fractions.
34    PiFractionPreferred,
35}
36
37/// Options for visualization parameter formatting.
38///
39/// Controls numeric precision, scientific-notation thresholds, and π-fraction matching.
40///
41/// # Examples
42///
43/// ```rust
44/// use cqlib_core::visualization::{ParameterDisplayMode, ParameterFormatOptions};
45///
46/// let options = ParameterFormatOptions {
47///     mode: ParameterDisplayMode::PiFractionPreferred,
48///     decimal_precision: 3,
49///     ..ParameterFormatOptions::default()
50/// };
51/// assert_eq!(options.decimal_precision, 3);
52/// ```
53#[derive(Debug, Clone, Copy)]
54pub struct ParameterFormatOptions {
55    /// Display mode strategy.
56    pub mode: ParameterDisplayMode,
57    /// Decimal precision for fixed-point/scientific formatting.
58    pub decimal_precision: usize,
59    /// Values in `(0, scientific_lower_bound)` use scientific notation.
60    pub scientific_lower_bound: f64,
61    /// Values `>= scientific_upper_bound` use scientific notation.
62    pub scientific_upper_bound: f64,
63    /// Tolerance when matching `value / π` to rational fractions.
64    pub pi_tolerance: f64,
65    /// Maximum denominator used for π fraction matching.
66    pub pi_max_denominator: i64,
67}
68
69impl Default for ParameterFormatOptions {
70    fn default() -> Self {
71        Self {
72            mode: ParameterDisplayMode::Numeric,
73            decimal_precision: 2,
74            scientific_lower_bound: 1e-3,
75            scientific_upper_bound: 1e4,
76            pi_tolerance: 1e-3,
77            pi_max_denominator: 16,
78        }
79    }
80}
81
82/// Parameter formatter used by visualization IR builder.
83///
84/// The formatter resolves fixed and symbolic circuit parameters into display labels according
85/// to [`ParameterFormatOptions`].
86#[derive(Debug, Clone, Copy)]
87pub struct ParameterFormatter {
88    options: ParameterFormatOptions,
89}
90
91impl ParameterFormatter {
92    /// Create a formatter with the given options.
93    ///
94    /// # Arguments
95    ///
96    /// * `options` - Parameter display and numeric-formatting policy.
97    ///
98    /// # Returns
99    ///
100    /// A formatter that can convert fixed or symbolic circuit parameters into display labels.
101    pub fn new(options: ParameterFormatOptions) -> Self {
102        Self { options }
103    }
104
105    /// Format one circuit parameter entry.
106    ///
107    /// # Arguments
108    ///
109    /// * `circuit` - Circuit that owns the symbolic parameter table.
110    /// * `param` - Fixed value or symbolic parameter reference to format.
111    ///
112    /// # Returns
113    ///
114    /// A short label suitable for text and figure rendering.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`VisualizationError::ParameterIndexOutOfBounds`] when a symbolic index is invalid.
119    pub fn format_circuit_param(
120        &self,
121        circuit: &Circuit,
122        param: &CircuitParam,
123    ) -> Result<String, VisualizationError> {
124        match param {
125            CircuitParam::Fixed(v) => Ok(self.format_from_expr_and_value(None, Some(*v))),
126            CircuitParam::Index(idx) => {
127                let p = circuit.parameters().get_index(*idx as usize).ok_or(
128                    VisualizationError::ParameterIndexOutOfBounds {
129                        index: *idx,
130                        len: circuit.parameters().len(),
131                    },
132                )?;
133                Ok(self.format_parameter(p))
134            }
135        }
136    }
137
138    fn format_parameter(&self, parameter: &Parameter) -> String {
139        let expr = parameter.to_string();
140        let value = parameter.evaluate(&None).ok();
141        self.format_from_expr_and_value(Some(expr.as_str()), value)
142    }
143
144    fn format_from_expr_and_value(&self, expr: Option<&str>, value: Option<f64>) -> String {
145        match self.options.mode {
146            ParameterDisplayMode::Numeric => value
147                .map(|v| self.format_numeric(v))
148                .or_else(|| expr.map(|e| self.format_symbolic_expr(e)))
149                .unwrap_or_else(|| "0".to_string()),
150            ParameterDisplayMode::Symbolic => expr
151                .map(|e| self.format_symbolic_expr(e))
152                .or_else(|| value.map(|v| self.format_numeric(v)))
153                .unwrap_or_else(|| "0".to_string()),
154            ParameterDisplayMode::SymbolicWithValue => match (expr, value) {
155                (Some(e), Some(v)) => {
156                    let expr_str = self.format_symbolic_expr(e);
157                    let value_str = self.format_numeric(v);
158                    if expr_str == value_str {
159                        expr_str
160                    } else {
161                        format!("{expr_str} ≈ {value_str}")
162                    }
163                }
164                (Some(e), None) => self.format_symbolic_expr(e),
165                (None, Some(v)) => self.format_numeric(v),
166                (None, None) => "0".to_string(),
167            },
168            ParameterDisplayMode::PiFractionPreferred => value
169                .and_then(|v| self.format_pi_fraction(v))
170                .or_else(|| value.map(|v| self.format_numeric(v)))
171                .or_else(|| expr.map(|e| self.format_symbolic_expr(e)))
172                .unwrap_or_else(|| "0".to_string()),
173        }
174    }
175
176    /// Normalize numeric literals embedded in symbolic expressions without touching identifiers.
177    fn format_symbolic_expr(&self, expr: &str) -> String {
178        let bytes = expr.as_bytes();
179        let mut out = String::with_capacity(expr.len());
180        let mut i = 0usize;
181
182        while i < bytes.len() {
183            // Treat a numeric token as standalone only when both neighbors are
184            // outside identifier syntax, so names such as `x_11` stay intact.
185            if is_number_start(bytes, i)
186                && !is_ascii_identifier_byte(previous_byte(bytes, i).unwrap_or_default())
187            {
188                let end = scan_number_literal(bytes, i);
189                let next = bytes.get(end).copied().unwrap_or_default();
190                if !is_ascii_identifier_byte(next) {
191                    let token = &expr[i..end];
192                    if let Ok(value) = token.parse::<f64>() {
193                        out.push_str(&self.format_numeric(value));
194                        i = end;
195                        continue;
196                    }
197                }
198            }
199
200            let ch = expr[i..].chars().next().unwrap_or_default();
201            out.push(ch);
202            i += ch.len_utf8();
203        }
204
205        out
206    }
207
208    fn format_pi_fraction(&self, value: f64) -> Option<String> {
209        if !value.is_finite() {
210            return None;
211        }
212        if value == 0.0 {
213            return Some("0".to_string());
214        }
215
216        let ratio = value / PI;
217        let max_den = self.options.pi_max_denominator.max(1);
218        let mut best: Option<(i64, i64, f64)> = None;
219
220        for den in 1..=max_den {
221            let num = (ratio * den as f64).round() as i64;
222            if num == 0 {
223                continue;
224            }
225            let approx = num as f64 / den as f64;
226            let err = (ratio - approx).abs();
227            if err > self.options.pi_tolerance {
228                continue;
229            }
230
231            let divisor = gcd_i64(num.abs(), den);
232            let reduced_num = num / divisor;
233            let reduced_den = den / divisor;
234            match best {
235                None => best = Some((reduced_num, reduced_den, err)),
236                Some((b_num, b_den, b_err)) => {
237                    if err < b_err
238                        || (err == b_err
239                            && (reduced_den < b_den
240                                || (reduced_den == b_den && reduced_num.abs() < b_num.abs())))
241                    {
242                        best = Some((reduced_num, reduced_den, err));
243                    }
244                }
245            }
246        }
247
248        let (num, den, _) = best?;
249        Some(format_pi_ratio(num, den))
250    }
251
252    fn format_numeric(&self, value: f64) -> String {
253        if value.is_nan() || value.is_infinite() {
254            return value.to_string();
255        }
256
257        let abs = value.abs();
258        if abs > 0.0
259            && (abs < self.options.scientific_lower_bound
260                || abs >= self.options.scientific_upper_bound)
261        {
262            return format_scientific(value, self.options.decimal_precision.max(1));
263        }
264
265        let mut s = format!("{:.*}", self.options.decimal_precision, value);
266        if self.options.decimal_precision > 0 {
267            s = s.trim_end_matches('0').trim_end_matches('.').to_string();
268        }
269        if s == "-0" {
270            s = "0".to_string();
271        }
272        if s == "0" && value != 0.0 {
273            return format_scientific(value, self.options.decimal_precision.max(1));
274        }
275        s
276    }
277}
278
279fn format_pi_ratio(num: i64, den: i64) -> String {
280    if den == 1 {
281        return match num {
282            1 => "π".to_string(),
283            -1 => "-π".to_string(),
284            _ => format!("{num}π"),
285        };
286    }
287    match num {
288        1 => format!("π/{den}"),
289        -1 => format!("-π/{den}"),
290        _ => format!("{num}π/{den}"),
291    }
292}
293
294fn format_scientific(value: f64, precision: usize) -> String {
295    let s = format!("{:.*e}", precision, value);
296    let (mantissa, exponent) = match s.split_once('e') {
297        Some(parts) => parts,
298        None => return s,
299    };
300    let mut mantissa = mantissa
301        .trim_end_matches('0')
302        .trim_end_matches('.')
303        .to_string();
304    if mantissa == "-0" {
305        mantissa = "0".to_string();
306    }
307    let exp_value = exponent.parse::<i32>().unwrap_or(0);
308    format!("{mantissa}e{exp_value}")
309}
310
311/// Return the previous byte when scanning ASCII-oriented symbolic expressions.
312fn previous_byte(bytes: &[u8], index: usize) -> Option<u8> {
313    index.checked_sub(1).and_then(|idx| bytes.get(idx)).copied()
314}
315
316/// Return whether a byte belongs to the identifier syntax used by parameters.
317fn is_ascii_identifier_byte(byte: u8) -> bool {
318    byte.is_ascii_alphanumeric() || byte == b'_'
319}
320
321/// Return whether the current byte starts a decimal number literal.
322fn is_number_start(bytes: &[u8], index: usize) -> bool {
323    bytes[index].is_ascii_digit()
324        || (bytes[index] == b'.' && bytes.get(index + 1).is_some_and(u8::is_ascii_digit))
325}
326
327/// Scan a decimal or scientific-notation literal and return its exclusive end.
328fn scan_number_literal(bytes: &[u8], start: usize) -> usize {
329    let mut end = start;
330
331    while bytes.get(end).is_some_and(u8::is_ascii_digit) {
332        end += 1;
333    }
334
335    if bytes.get(end) == Some(&b'.') {
336        end += 1;
337        while bytes.get(end).is_some_and(u8::is_ascii_digit) {
338            end += 1;
339        }
340    }
341
342    if matches!(bytes.get(end), Some(b'e' | b'E')) {
343        let exp_start = end;
344        let mut exp_end = end + 1;
345        if matches!(bytes.get(exp_end), Some(b'+' | b'-')) {
346            exp_end += 1;
347        }
348        let digit_start = exp_end;
349        while bytes.get(exp_end).is_some_and(u8::is_ascii_digit) {
350            exp_end += 1;
351        }
352        if exp_end > digit_start {
353            end = exp_end;
354        } else {
355            end = exp_start;
356        }
357    }
358
359    end
360}
361
362fn gcd_i64(mut a: i64, mut b: i64) -> i64 {
363    while b != 0 {
364        let r = a % b;
365        a = b;
366        b = r;
367    }
368    a.abs().max(1)
369}