use super::options::{PlotSeries, ResultPlotKind, ResultPlotOptions};
use super::plot::render_bar_svg;
use crate::device::ExecutionResult;
use crate::visualization::VisualizationError;
use std::collections::BTreeSet;
pub(crate) struct PreparedResultPlot {
pub(crate) labels: Vec<String>,
pub(crate) values: Vec<Vec<f64>>,
pub(crate) kind: ResultPlotKind,
}
pub fn plot_histogram(
result: &ExecutionResult,
options: &ResultPlotOptions,
) -> Result<String, VisualizationError> {
let series = execution_result_to_plot_series(result)?;
let prepared = prepare_result_plot(&[series], options, ResultPlotKind::Histogram)?;
Ok(render_bar_svg(&prepared, options))
}
pub fn plot_distribution(
result: &ExecutionResult,
options: &ResultPlotOptions,
) -> Result<String, VisualizationError> {
let series = execution_result_to_plot_series(result)?;
let prepared = prepare_result_plot(&[series], options, ResultPlotKind::Distribution)?;
Ok(render_bar_svg(&prepared, options))
}
pub(crate) fn execution_result_to_plot_series(
result: &ExecutionResult,
) -> Result<PlotSeries, VisualizationError> {
if result.counts().is_empty() {
return Err(VisualizationError::InvalidInput(
"execution result contains no counts to plot".to_string(),
));
}
let num_qubits = result.num_qubits();
Ok(result
.counts()
.iter()
.map(|(outcome, count)| (outcome.to_bitstring(num_qubits), *count as f64))
.collect())
}
pub(crate) fn prepare_result_plot(
data: &[PlotSeries],
options: &ResultPlotOptions,
kind: ResultPlotKind,
) -> Result<PreparedResultPlot, VisualizationError> {
validate_inputs(data, options)?;
let mut datasets = data.to_vec();
if kind == ResultPlotKind::Distribution {
datasets = datasets
.into_iter()
.map(normalize_distribution)
.collect::<Result<Vec<_>, _>>()?;
}
if let Some(k) = options.number_to_keep {
datasets = datasets
.into_iter()
.map(|dataset| keep_topk_with_rest(&dataset, k))
.collect();
}
let labels = sorted_labels(&datasets, options)?;
let values = datasets
.iter()
.map(|dataset| {
labels
.iter()
.map(|label| *dataset.get(label).unwrap_or(&0.0))
.collect()
})
.collect();
Ok(PreparedResultPlot {
labels,
values,
kind,
})
}
fn validate_inputs(
data: &[PlotSeries],
options: &ResultPlotOptions,
) -> Result<(), VisualizationError> {
if data.is_empty() {
return Err(VisualizationError::InvalidInput(
"plot data must contain at least one dataset".to_string(),
));
}
if let Some(legend) = &options.legend
&& legend.len() != data.len()
{
return Err(VisualizationError::InvalidInput(format!(
"legend length ({}) does not match dataset count ({})",
legend.len(),
data.len()
)));
}
Ok(())
}
pub(crate) fn normalize_distribution(data: PlotSeries) -> Result<PlotSeries, VisualizationError> {
let total: f64 = data.values().sum();
if total.abs() < 1e-12 {
return Err(VisualizationError::InvalidInput(
"distribution data sum is zero".to_string(),
));
}
Ok(data
.into_iter()
.map(|(label, value)| (label, value / total))
.collect())
}
pub(crate) fn keep_topk_with_rest(data: &PlotSeries, k: usize) -> PlotSeries {
if k == 0 || k >= data.len() {
return data.clone();
}
let mut items: Vec<_> = data
.iter()
.map(|(label, value)| (label.clone(), *value))
.collect();
items.sort_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
let rest = items[..items.len() - k]
.iter()
.map(|(_, value)| *value)
.sum();
let mut out = items[items.len() - k..]
.iter()
.cloned()
.collect::<PlotSeries>();
out.insert("rest".to_string(), rest);
out
}
pub(crate) fn sorted_labels(
datasets: &[PlotSeries],
options: &ResultPlotOptions,
) -> Result<Vec<String>, VisualizationError> {
let mut labels: Vec<String> = datasets
.iter()
.flat_map(|dataset| dataset.keys().cloned())
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
match options.sort.as_str() {
"asc" => {}
"desc" => labels.reverse(),
"value" => {
labels.sort_by(|a, b| {
let av = peak_value(datasets, a);
let bv = peak_value(datasets, b);
av.total_cmp(&bv).then_with(|| a.cmp(b))
});
}
"value_desc" => {
labels.sort_by(|a, b| {
let av = peak_value(datasets, a);
let bv = peak_value(datasets, b);
bv.total_cmp(&av).then_with(|| a.cmp(b))
});
}
"hamming" => {
let target = options.target_string.as_ref().ok_or_else(|| {
VisualizationError::InvalidInput(
"sort='hamming' requires target_string".to_string(),
)
})?;
labels.sort_by(|a, b| {
if a == "rest" {
return std::cmp::Ordering::Greater;
}
if b == "rest" {
return std::cmp::Ordering::Less;
}
let da = hamming_distance(a, target).unwrap_or(usize::MAX);
let db = hamming_distance(b, target).unwrap_or(usize::MAX);
da.cmp(&db).then_with(|| a.cmp(b))
});
for label in labels.iter().filter(|label| label.as_str() != "rest") {
hamming_distance(label, target)?;
}
}
other => {
return Err(VisualizationError::InvalidInput(format!(
"sort must be one of asc, desc, value, value_desc, hamming; got '{other}'"
)));
}
}
Ok(labels)
}
fn peak_value(datasets: &[PlotSeries], label: &str) -> f64 {
datasets
.iter()
.map(|dataset| dataset.get(label).copied().unwrap_or(0.0))
.fold(f64::NEG_INFINITY, f64::max)
}
pub(crate) fn hamming_distance(a: &str, b: &str) -> Result<usize, VisualizationError> {
if a.len() != b.len() {
return Err(VisualizationError::InvalidInput(
"hamming sort needs equal-length bitstrings".to_string(),
));
}
Ok(a.chars().zip(b.chars()).filter(|(x, y)| x != y).count())
}