Skip to content
Closed
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