Skip to content
Merged
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
241 changes: 154 additions & 87 deletions src/components/StatusModal/CalibrationTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,116 @@ import React from 'react'
import { CTable } from '@cscfi/csc-ui-react';
import { formatMetricValue } from '../../utils/formatMetricValue';

const QUBIT_SEGMENT = /^QB(\d+)$/;

// Sort on the numeric segments so ids that aren't QBx__QBy (e.g. VLQ's
// QBx__COMPR1 and QBx__COMPR1__QBanchor) still order sensibly.
const compareIds = (a, b) => {
const numsA = (a.match(/\d+/g) || []).map(Number);
const numsB = (b.match(/\d+/g) || []).map(Number);
for (let i = 0; i < Math.max(numsA.length, numsB.length); i++) {
const diff = (numsA[i] ?? -1) - (numsB[i] ?? -1);
if (diff) return diff;
}
return a.localeCompare(b);
};

// Collapse the two orientations the backend may use for a qubit pair onto one row.
const normalizeId = (id) => {
const match = id.match(/^QB(\d+)__QB(\d+)$/);
if (!match) return id;
const [low, high] = [Number(match[1]), Number(match[2])].sort((a, b) => a - b);
return `QB${low}__QB${high}`;
};

// Ids of the same shape get their own table: QBx__COMPR1 and QBx__COMPR1__QBy
// are different kinds of coupler and carry disjoint metrics.
const shapeOf = (id) => id.split('__')
.map(segment => (QUBIT_SEGMENT.test(segment) ? 'QB' : segment))
.join(' ↔ ');

// A metric keyed on three or more plain qubits describes a group of qubits
// rather than a coupler between them.
const isMultiQubit = (id) => {
const segments = id.split('__');
return segments.length > 2 && segments.every(segment => QUBIT_SEGMENT.test(segment));
};

const multiQubitLabel = (id) => {
const nums = id.split('__').map(segment => Number(segment.replace('QB', '')));
const contiguous = nums.every((num, i) => i === 0 || num === nums[i - 1] + 1);
return contiguous
? `QB${nums[0]}–QB${nums[nums.length - 1]} (${nums.length} qubits)`
: `${nums.length} qubits`;
};

const lookupEntry = (calibrationData, metric, id) => {
const entry = calibrationData[metric]?.[id];
if (entry?.value !== null && entry?.value !== undefined) return entry;
const match = id.match(/^QB(\d+)__QB(\d+)$/);
if (match) {
const flipped = calibrationData[metric]?.[`QB${match[2]}__QB${match[1]}`];
if (flipped?.value !== null && flipped?.value !== undefined) return flipped;
}
return entry;
};

const Cell = ({ entry }) => (
<td>{entry?.value !== null && entry?.value !== undefined ? formatMetricValue(entry.value, entry.unit || '') : 'N/A'}</td>
);

const MetricTable = ({ ids, idMap, calibrationData, metrics, metricLabel }) => (
<CTable>
<table>
<thead>
<tr>
<th className='!font-bold'>ID</th>
{metrics.map(metric => (
<th className='!font-bold' key={metric}>{metricLabel(metric)}</th>
))}
</tr>
</thead>
<tbody>
{ids.map(id => (
<tr key={id}>
<td>{id}</td>
{metrics.map(metric => (
<Cell key={`${id}-${metric}`} entry={lookupEntry(calibrationData, metric, idMap[id])} />
))}
</tr>
))}
</tbody>
</table>
</CTable>
);

// Transposed: one column per qubit group, since a group's id is too long to
// read as a row label.
const MultiQubitTable = ({ ids, idMap, calibrationData, metrics, metricLabel }) => (
<CTable>
<table>
<thead>
<tr>
<th className='!font-bold'>Metric</th>
{ids.map(id => (
<th className='!font-bold' key={id} title={idMap[id]}>{multiQubitLabel(id)}</th>
))}
</tr>
</thead>
<tbody>
{metrics.map(metric => (
<tr key={metric}>
<td>{metricLabel(metric)}</td>
{ids.map(id => (
<Cell key={`${id}-${metric}`} entry={lookupEntry(calibrationData, metric, idMap[id])} />
))}
</tr>
))}
</tbody>
</table>
</CTable>
);

// Render calibration data as a table
export const CalibrationTable = (props) => {

Expand All @@ -11,100 +121,57 @@ export const CalibrationTable = (props) => {
const allMetrics = Object.keys(calibrationData);
if (allMetrics.length === 0) return <p>No metrics available</p>;

// Get all unique qubit/coupler IDs
const allIds = new Set();
const idMap = {}; // Map normalized IDs to actual IDs in data
// Map normalized IDs to the actual IDs in the data
const idMap = {};
allMetrics.forEach(metric => {
Object.keys(calibrationData[metric]).forEach(id => {
if (id !== 'statistics' && ((qubitSwitch && !id.includes("__")) || (!qubitSwitch && id.includes("__")))) {
// Normalize the ID
const match = id.match(/QB(\d+)__QB(\d+)/);
let normalizedId = id;
if (match) {
const [, num1, num2] = match;
const smaller = Math.min(parseInt(num1), parseInt(num2));
const larger = Math.max(parseInt(num1), parseInt(num2));
normalizedId = `QB${smaller}__QB${larger}`;
}
allIds.add(normalizedId);
idMap[normalizedId] = id; // Store the actual ID
}
if (id === 'statistics') return;
if (qubitSwitch === id.includes('__')) return;
idMap[normalizeId(id)] = id;
});
});

var sortedIds;
if (qubitSwitch){
sortedIds = Array.from(allIds).sort((a, b) => {
const numA = parseInt(a.replace('QB', ''));
const numB = parseInt(b.replace('QB', ''));
return numA - numB;
});
} else {
sortedIds = Array.from(allIds).sort((a, b) => {
const [a1, a2] = a.match(/QB(\d+)__QB(\d+)/).slice(1).map(Number);
const [b1, b2] = b.match(/QB(\d+)__QB(\d+)/).slice(1).map(Number);
return a1 - b1 || a2 - b2;
});
}
// Filter out metrics that have only N/A values for the filtered IDs
const metrics = allMetrics.filter(metric => {
return sortedIds.some(id => {
const actualId = idMap[id];
const data = calibrationData[metric][actualId];
const value = data?.value;
return value !== null && value !== undefined;
});
const sortedIds = Object.keys(idMap).sort(compareIds);
if (sortedIds.length === 0) return <p>No metrics available</p>;

const metricLabel = (metric) =>
qubitMetricOptions.find(m => m.value === metric)?.name ||
couplerMetricOptions.find(m => m.value === metric)?.name ||
metric;

// Drop metrics that have no value for any of these IDs
const metricsFor = (ids) => allMetrics.filter(metric => ids.some(id => {
const value = lookupEntry(calibrationData, metric, idMap[id])?.value;
return value !== null && value !== undefined;
}));

const multiQubitIds = qubitSwitch ? [] : sortedIds.filter(isMultiQubit);
const groups = [];
sortedIds.filter(id => !multiQubitIds.includes(id)).forEach(id => {
const shape = shapeOf(id);
const group = groups.find(g => g.shape === shape);
if (group) group.ids.push(id);
else groups.push({ shape, ids: [id] });
});
groups.sort((a, b) =>
a.ids[0].split('__').length - b.ids[0].split('__').length || a.shape.localeCompare(b.shape));

const tableProps = { idMap, calibrationData, metricLabel };

return (
<div className="overflow-auto max-h-[50vh]">
<CTable>
<table >
<thead >
<tr>
<th className='!font-bold' >ID</th>
{metrics.map(metric => (
<th className='!font-bold' key={metric}>
{qubitMetricOptions.find(m => m.value === metric)?.name ||
couplerMetricOptions.find(m => m.value === metric)?.name ||
metric}
</th>
))}
</tr>
</thead>
<tbody>
{sortedIds.map(id => (
<tr key={id}>
<td>{id}</td>
{metrics.map(metric => {
const actualId = idMap[id];
let data = calibrationData[metric][actualId];
let value = data?.value;
const unit = data?.unit || '';

// Try flipped ID if value is N/A
if ((value === null || value === undefined) && actualId.includes("__")) {
const match = actualId.match(/QB(\d+)__QB(\d+)/);
if (match) {
const flippedId = `QB${match[2]}__QB${match[1]}`;
const flippedData = calibrationData[metric][flippedId];
if (flippedData?.value !== null && flippedData?.value !== undefined) {
value = flippedData.value;
}
}
}

return (
<td key={`${id}-${metric}`}>
{value !== null && value !== undefined ? formatMetricValue(value, unit) : 'N/A'}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</CTable>
<div className="overflow-auto max-h-[50vh] flex flex-col gap-6">
{groups.map(group => (
<div key={group.shape} className='flex flex-col gap-2'>
<p className='font-bold text-[14px]'>{group.shape}</p>
<MetricTable {...tableProps} ids={group.ids} metrics={metricsFor(group.ids)} />
</div>
))}
{multiQubitIds.length > 0 && (
<div className='flex flex-col gap-2'>
<p className='font-bold text-[14px]'>Multi-qubit metrics</p>
<MultiQubitTable {...tableProps} ids={multiQubitIds} metrics={metricsFor(multiQubitIds)} />
</div>
)}
</div>
);
};
};
Loading