cqlib_core/visualization/circuit/
params.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ParameterDisplayMode {
27 Numeric,
29 Symbolic,
31 SymbolicWithValue,
33 PiFractionPreferred,
35}
36
37#[derive(Debug, Clone, Copy)]
54pub struct ParameterFormatOptions {
55 pub mode: ParameterDisplayMode,
57 pub decimal_precision: usize,
59 pub scientific_lower_bound: f64,
61 pub scientific_upper_bound: f64,
63 pub pi_tolerance: f64,
65 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#[derive(Debug, Clone, Copy)]
87pub struct ParameterFormatter {
88 options: ParameterFormatOptions,
89}
90
91impl ParameterFormatter {
92 pub fn new(options: ParameterFormatOptions) -> Self {
102 Self { options }
103 }
104
105 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 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 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
311fn previous_byte(bytes: &[u8], index: usize) -> Option<u8> {
313 index.checked_sub(1).and_then(|idx| bytes.get(idx)).copied()
314}
315
316fn is_ascii_identifier_byte(byte: u8) -> bool {
318 byte.is_ascii_alphanumeric() || byte == b'_'
319}
320
321fn 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
327fn 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}