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
24 changes: 24 additions & 0 deletions content/_data/constants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ cookie_consent:
- link: "https://qx.vtt.fi/docs/devices/q50.html"
teaser: "Read more about VTT Q50 (VTT website)"
icon: mdiOpenInNew

- name: EuroHPC VLQ
desc: |-
EuroHPC VLQ is a superconducting IQM quantum computer with 24 qubits in a star topology. It is operated by IT4I.
image: "/assets/images/vlq-images/vlq-image.jpg"
links:
- link: "/resource-call"
teaser: "Apply for access to EuroHPC VLQ"
- link: "https://docs.csc.fi/computing/quantum-computing/running-quantum-jobs/"
teaser: "How to access EuroHPC VLQ, instructions"
icon: mdiArrowRight
- link: "https://docs.it4i.cz/en/docs/clusters/vlq/introduction"
teaser: "Read more about EuroHPC VLQ (IT4I website)"
icon: mdiOpenInNew

- name: VTT Q5 (Helmi) - End of life
desc: |-
Expand Down Expand Up @@ -256,6 +270,9 @@ cookie_consent:
Aalto Q20 access is currently being rolled out! See the CSC Quantum Computing docs for more info.
link: "https://docs.csc.fi/computing/quantum-computing/overview/"
type: info
- text: |-
Access to EuroHPC VLQ is coming soon!
type: info
quantum-computers:
- name: VTT Q50
qubits: 53
Expand All @@ -271,6 +288,13 @@ cookie_consent:
pulse: "True"
device_id: "Q20"

- name: VLQ
qubits: 24
basis: "PRX, CZ, MOVE"
topology: "Star"
pulse: "True"
device_id: "VLQ"

"/cookies/":
title: Cookie Policy
desc: |-
Expand Down
Binary file added content/assets/images/vlq-images/vlq-image.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions src/components/Loading.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react'
import { CSpinner } from '@cscfi/csc-ui-react'

/**
* Stand-in for the Online/Offline status pill. Keeps the pill's height so the
* card doesn't jump when the healthcheck resolves.
*/
export const StatusPillLoading = () => (
<div className='text-center text-[#3F3F3F] bg-[#E4E4E4] border-[0.5px] border-[#3F3F3F] rounded-[100px] w-[88px] h-[25px] animate-pulse'>
<p className='font-bold text-[14px]'>Loading...</p>
</div>
)

/** Centered spinner for a panel whose data hasn't arrived yet. */
export const LoadingBlock = (props) => (
<div className={`flex items-center justify-center gap-3 py-8 ${props.className || ''}`}>
<CSpinner size={24} width={2} />
<p className='text-[14px] text-gray-600'>{props.label || 'Loading…'}</p>
</div>
)

/** Shown when a fetch failed, in the same slot the LoadingBlock occupied. */
export const ErrorBlock = (props) => (
<div className={`flex items-center justify-center py-8 ${props.className || ''}`}>
<p className='text-[14px] text-[#7E0707]'>{props.label || 'Data is currently unavailable.'}</p>
</div>
)
158 changes: 118 additions & 40 deletions src/components/QcLayouts/QcLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ const NODE_UNITS = 90;
const MAX_NODE_PX = 48;

export function QcLayout({ layout, metrics }) {
const { spacing, nodes, edges } = layout;
const { spacing, nodes, edges, resonator } = layout;
// Star layouts (with a central resonator) render qubits as upright squares;
// lattice layouts render them as 45°-rotated diamonds.
const nodeRotation = resonator ? 0 : 45;
const {
calibrationData, qubitMetric, couplerMetric,
qubitMetricFormatted, couplerMetricFormatted, thresholdQubit, thresholdCoupler,
qubitMetricFormatted, couplerMetricFormatted,
thresholdQubit, thresholdCoupler,
} = metrics;

const [hoveredNode, setHoveredNode] = useState(null);
Expand All @@ -41,16 +45,26 @@ export function QcLayout({ layout, metrics }) {
return entry?.unit || '';
};

// Resolve a coupler key from the two endpoints (try both orderings)
// Resolve a coupler key from the two endpoints (try both orderings).
const couplerKey = (a, b) => {
if (getMetricValue(couplerMetric, `${a}__${b}`) !== null) return `${a}__${b}`;
return `${b}__${a}`;
if (getMetricValue(couplerMetric, `${b}__${a}`) !== null) return `${b}__${a}`;
// Star MOVE/CZ gates routed through the resonator to a fixed anchor qubit
// are keyed QBx__RES__QBanchor. The edge is (QBx, RES): find the matching
// key so the gate's value renders on that edge (and shows on hover).
if (resonator && calibrationData?.[couplerMetric]) {
const qubit = a === resonator.id ? b : a;
const prefix = `${qubit}__${resonator.id}__`;
const match = Object.keys(calibrationData[couplerMetric]).find(k => k.startsWith(prefix));
if (match) return match;
}
return `${a}__${b}`;
};

// Color for a metric value. dim: 1 = qubit, 2 = coupler.
// Color for a metric value. dim: 1 = qubit, 2 = coupler, 3 = resonator.
const getColor = (metric, id, dim, threshold) => {
// No metric selected: qubits get the brand blue, couplers a light grey
if (!metric || metric === '') return dim === 1 ? DEFAULT_NODE_COLOR : '#aaa';
// No metric selected: qubits/resonator get the brand blue, couplers a light grey
if (!metric || metric === '') return dim === 2 ? '#aaa' : DEFAULT_NODE_COLOR;
if (!calibrationData || !calibrationData[metric]) return GREY;

const value = getMetricValue(metric, id);
Expand Down Expand Up @@ -108,8 +122,6 @@ export function QcLayout({ layout, metrics }) {
if (!containerRef.current || !tooltip) return { left: 0, top: 0 };

const containerRect = containerRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

let tooltipWidth = 300; // rough estimate
let tooltipHeight = 150; // rough estimate
Expand All @@ -119,54 +131,81 @@ export function QcLayout({ layout, metrics }) {
tooltipHeight = 70; // rough estimate
}

// Calculate absolute position on screen
const absoluteX = containerRect.left + mousePos.x;
const absoluteY = containerRect.top + mousePos.y;

let left = mousePos.x + 8;
let top = mousePos.y + 8;

// Check right boundary against viewport
if (absoluteX + 8 + tooltipWidth > viewportWidth) {
left = mousePos.x - tooltipWidth - 8; // Position to the left of cursor
// Horizontal: keep the tooltip's right edge within the layout area so it
// never spills past the right edge. If it would, flip it to the left of
// the cursor; never let it run off the left of the viewport.
const maxLeft = containerRect.width - tooltipWidth - 8; // relative to container
if (left > maxLeft) {
left = mousePos.x - tooltipWidth - 8; // flip to the left of the cursor
}

// Check bottom boundary against viewport
if (absoluteY + 8 + tooltipHeight > viewportHeight) {
top = mousePos.y - tooltipHeight - 8; // Position above cursor
const minLeft = 8; // keep the tooltip's left edge inside the layout area
left = Math.min(left, maxLeft);
left = Math.max(left, minLeft);

// Vertical: keep the tooltip's bottom within the layout area so it never
// spills past the bottom edge. If it would, flip it above the cursor; let
// it extend upward (the modal has room above) but never above the viewport.
const maxTop = containerRect.height - tooltipHeight - 8; // relative to container
if (top > maxTop) {
top = mousePos.y - tooltipHeight - 8; // flip above the cursor
}

// Ensure tooltip stays within container bounds
left = Math.max(8, Math.min(left, containerRect.width - tooltipWidth - 8));
top = Math.max(8, Math.min(top, containerRect.height - tooltipHeight - 8));
const minTop = 8; // keep the tooltip's top edge inside the layout area
top = Math.min(top, maxTop);
top = Math.max(top, minTop);

return { left, top };
};

// Build coordinate map directly from nodes
const coordMap = Object.fromEntries(nodes.map(n => [n.id, n]));

// Compute dynamic bounds
// Resolve an edge's two endpoints. A star-layout edge connects a qubit to the
// central resonator: that endpoint isn't a node, so it maps to the point on the
// resonator bar directly above/below the qubit (a vertical coupler).
const endpoint = (id, other) => {
if (resonator && id === resonator.id) return { x: coordMap[other].x, y: resonator.y };
return coordMap[id];
};

// Compute dynamic bounds (include the resonator bar if present)
const xs = nodes.map(n => n.x);
const ys = nodes.map(n => n.y);
const minX = Math.min(...xs) - spacing;
const maxX = Math.max(...xs) + spacing;
const minY = Math.min(...ys) - spacing;
const maxY = Math.max(...ys) + spacing;
if (resonator) {
xs.push(resonator.x1, resonator.x2);
ys.push(resonator.y);
}
// Margin around the outermost nodes. Star layouts pack tightly (upright
// squares), so they only need a node-sized margin; lattice layouts keep a
// full spacing of breathing room.
const margin = resonator ? NODE_UNITS * 0.75 : spacing;
const minX = Math.min(...xs) - margin;
const maxX = Math.max(...xs) + margin;
const minY = Math.min(...ys) - margin;
const maxY = Math.max(...ys) + margin;
const viewBoxWidth = maxX - minX;
const viewBoxHeight = maxY - minY;
const viewBox = `${minX} ${-maxY} ${viewBoxWidth} ${viewBoxHeight}`;

// Cap the rendered size so an SVG unit never maps to more pixels than
// MAX_NODE_PX / NODE_UNITS — i.e. nodes/couplers stay the same on-screen size
// across layouts. Use the larger viewBox dimension so neither axis overshoots.
const maxWidth = (MAX_NODE_PX / NODE_UNITS) * Math.max(viewBoxWidth, viewBoxHeight);
// across layouts. The container matches the viewBox aspect ratio so a wide,
// short layout (e.g. the star) isn't padded out to a tall square.
const pxPerUnit = MAX_NODE_PX / NODE_UNITS;
const maxRenderWidth = pxPerUnit * viewBoxWidth;
const maxRenderHeight = pxPerUnit * viewBoxHeight;

return (
<div
ref={containerRef}
className="relative w-full aspect-square overflow-hidden flex justify-center items-center mx-auto"
style={{ maxWidth: `min(${maxWidth}px, 60vh)`, maxHeight: `min(${maxWidth}px, 60vh)` }}
className="relative w-full overflow-hidden flex justify-center items-center mx-auto"
style={{
aspectRatio: `${viewBoxWidth} / ${viewBoxHeight}`,
maxWidth: `${maxRenderWidth}px`,
maxHeight: `${maxRenderHeight}px`,
}}
onMouseMove={handleMouseMove}
onMouseLeave={() => { setHoveredNode(null); setHoveredEdge(null); setTooltip(null); }}
>
Expand Down Expand Up @@ -206,11 +245,10 @@ export function QcLayout({ layout, metrics }) {
viewBox={viewBox}
preserveAspectRatio="xMidYMid meet"
className="w-full h-full"
style={{ maxWidth: `${maxWidth}px`, maxHeight: `${maxWidth}px` }}
>
>
{edges.map(([a, b]) => {
const A = coordMap[a];
const B = coordMap[b];
const A = endpoint(a, b);
const B = endpoint(b, a);
const key = `${a}-${b}`;
const hover = hoveredEdge === key;
const edgeColor = getColor(couplerMetric, couplerKey(a, b), 2, thresholdCoupler);
Expand All @@ -227,13 +265,53 @@ export function QcLayout({ layout, metrics }) {
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
onMouseEnter={() => {
setHoveredEdge(key);
setTooltip(buildTooltip('edge', couplerMetric, couplerKey(a, b), couplerMetricFormatted, `Coupler: ${a}__${b}`));
setTooltip(buildTooltip('edge', couplerMetric, couplerKey(a, b), couplerMetricFormatted, `Coupler: ${couplerKey(a, b)}`));
}}
onMouseLeave={() => { setHoveredEdge(null); setTooltip(null); }}
className={hover ? 'cursor-pointer filter drop-shadow-md' : 'cursor-pointer'}
/>
);
})}
{resonator && (() => {
const hover = hoveredEdge === resonator.id;
const barHeight = 90;
// The resonator has no dropdown of its own: its T1/T2 share the
// qubit keys, so the bar follows the selected qubit metric.
const barColor = getColor(qubitMetric, resonator.id, 1, thresholdQubit);
return (
<motion.g
initial={{ scale: 1 }}
animate={{ scale: hover ? 1.04 : 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
style={{ transformBox: 'fill-box', transformOrigin: 'center' }}
onMouseEnter={() => {
setHoveredEdge(resonator.id);
setTooltip(buildTooltip('resonator', qubitMetric, resonator.id, qubitMetricFormatted, `Resonator: ${resonator.id}`));
}}
onMouseLeave={() => { setHoveredEdge(null); setTooltip(null); }}
className={hover ? 'cursor-pointer filter drop-shadow-lg' : 'cursor-pointer'}
>
<rect
x={resonator.x1}
y={-resonator.y - barHeight / 2}
width={resonator.x2 - resonator.x1}
height={barHeight}
rx={10}
fill={barColor}
/>
<text
x={(resonator.x1 + resonator.x2) / 2}
y={-resonator.y}
fill="#fff"
fontFamily="monospace"
fontSize={40}
fontWeight="bold"
textAnchor="middle"
dominantBaseline="central"
>{resonator.id}</text>
</motion.g>
);
})()}
{nodes.map(n => {
const hover = hoveredNode === n.id;
const nodeColor = getColor(qubitMetric, n.id, 1, thresholdQubit);
Expand All @@ -250,7 +328,7 @@ export function QcLayout({ layout, metrics }) {
onMouseLeave={() => { setHoveredNode(null); setTooltip(null); }}
className={hover ? 'cursor-pointer filter drop-shadow-lg' : 'cursor-pointer'}
>
<g transform="rotate(45)">
<g transform={`rotate(${nodeRotation})`}>
<rect x={-45} y={-45} width={90} height={90} rx={10} fill={nodeColor} />
<text
x={0} y={0}
Expand All @@ -260,7 +338,7 @@ export function QcLayout({ layout, metrics }) {
fontWeight="bold"
textAnchor="middle"
dominantBaseline="central"
transform="rotate(-45)"
transform={`rotate(${-nodeRotation})`}
>{n.id}</text>
</g>
</motion.g>
Expand Down
47 changes: 47 additions & 0 deletions src/components/QcLayouts/layouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
// To add a new QC: add one entry to QC_LAYOUTS keyed by the lowercased device_id,
// with { spacing, nodes, edges }. No new component or call-site change is needed.
//
// Two topology shapes are supported:
// 1. Lattice — qubits coupled directly to neighbouring qubits. `edges` is a list
// of [qubitA, qubitB] pairs (see Q20/Q50).
// 2. Star — every qubit couples to a single central resonator providing
// one-to-all connectivity. Add a `resonator` descriptor and make
// each edge a [qubit, resonator.id] pair. The resonator renders as a central
// bar and each edge as a coupler from its qubit to that bar.
//
// Nodes are positioned on a diagonal grid. `grid(xOrigin, yOrigin, spacing)` returns
// a helper `(id, col, row) => { id, x, y }` so coordinates read as grid offsets instead
// of repeated `xOrigin + spacing * N` arithmetic.
Expand Down Expand Up @@ -88,7 +96,46 @@ const q50Edges = [
['QB2', 'QB1'],
];

// --- VLQ (24-qubit IQM star) ----------------------------------------------
// One central computational resonator with every qubit coupled to it, giving
// one-to-all connectivity. Qubits sit in two rows above and below the resonator:
//
// O O O ... (12 qubits)
// | | |
// =========== <- central resonator
// | | |
// O O O ... (12 qubits)
//
// `RESONATOR_ID` must match the resonator's name in the calibration coupler keys
// (e.g. "QB1__COMPR1") and in any resonator-level metrics. It is also shown as
// the label on the resonator bar. Change it here if the backend uses another name.
const RESONATOR_ID = 'COMPR1';
const vlqSpacing = 140;
const vlqCols = 12; // 12 qubits per row, 24 total
const vlqDiamond = vlqSpacing * 0.45; // half a node's diagonal, for bar overlap

// Top row QB1..QB12 (left→right), bottom row QB13..QB24 (left→right).
const vlqNodes = [
...Array.from({ length: vlqCols }, (_, col) => ({
id: `QB${col + 1}`, x: col * vlqSpacing, y: vlqSpacing,
})),
...Array.from({ length: vlqCols }, (_, col) => ({
id: `QB${col + 1 + vlqCols}`, x: col * vlqSpacing, y: -vlqSpacing,
})),
];
// Central resonator bar, spanning slightly past the outermost qubit columns.
const vlqResonator = {
id: RESONATOR_ID,
label: 'Computational Resonator',
y: 0,
x1: -vlqDiamond,
x2: (vlqCols - 1) * vlqSpacing + vlqDiamond,
};
// Every qubit couples to the resonator.
const vlqEdges = vlqNodes.map(n => [n.id, RESONATOR_ID]);

export const QC_LAYOUTS = {
q20: { spacing: q20Spacing, nodes: q20Nodes, edges: q20Edges },
q50: { spacing: q50Spacing, nodes: q50Nodes, edges: q50Edges },
vlq: { spacing: vlqSpacing, nodes: vlqNodes, edges: vlqEdges, resonator: vlqResonator },
};
Loading
Loading