diff --git a/.gitignore b/.gitignore index 6d4a715e4dba..40ad8d4aeedb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ dist node_modules _config_webpack.yml -test_local/ \ No newline at end of file +test_local/ diff --git a/content/_data/constants.yml b/content/_data/constants.yml index 5e914763d80e..ad4360eb7f8e 100644 --- a/content/_data/constants.yml +++ b/content/_data/constants.yml @@ -72,7 +72,7 @@ cookie_consent: description: |- The Finnish quantum-computing infrastructure (FiQCI) provides access to quantum computers through the LUMI supercomputer environment. Currently there are two quantum computers - available for use: VTT Q5 (Helmi), and VTT Q50. + available for use: Aalto Q20 and VTT Q50. links: - title: Quantum computer resources href: /access/#quantum @@ -112,21 +112,16 @@ cookie_consent: href: https://fiqci.fi/resource-estimator quantum_resources: - - name: VTT Q5 (Helmi) + - name: Aalto Q20 desc: |- - The VTT Q5 "Helmi" is the first Finnish quantum computer. Helmi is based on superconducting technology and provides 5 qubits - in a star-shape topology. Hybrid HPC+QC access was opened in 2022. Helmi is operated by VTT, - and was co-developed with IQM Quantum Computers. - image: "/assets/images/vtt-images/VTT_lab-2.webp" + Aalto Q20 is an IQM superconducting quantum computer with 20 qubits in a square grid topology. It is operated by Aalto University. + image: "/assets/images/aalto-qc/aalto-q20.webp" links: - - link: "/publications/2022-11-01-Helmi_pilot" - teaser: "Apply for access to VTT Q5" + - link: "/publications/2026-05-11-Academic-Call-2026-02" + teaser: "Apply for access to Aalto Q20" - link: "https://docs.csc.fi/computing/quantum-computing/running-quantum-jobs/" - teaser: "How to access VTT Q5, instructions" + teaser: "How to access Aalto Q20, instructions" icon: mdiArrowRight - - link: "https://qx.vtt.fi/docs/devices/q5.html" - teaser: "Read more about VTT Q5 (VTT website)" - icon: mdiOpenInNew - name: VTT Q50 desc: |- @@ -142,6 +137,18 @@ cookie_consent: - link: "https://qx.vtt.fi/docs/devices/q50.html" teaser: "Read more about VTT Q50 (VTT website)" icon: mdiOpenInNew + + - name: VTT Q5 (Helmi) - End of life + desc: |- + VTT Q5 (Helmi) was a superconducting quantum computer with 5 qubits in a star topology. It was operated by VTT, and was co-developed with IQM Quantum Computers. The device was decommissioned in 2026. + image: "/assets/images/vtt-images/VTT_lab-2.webp" + links: + - link: "https://docs.csc.fi/computing/quantum-computing/running-quantum-jobs/" + teaser: "How to access VTT Q5 (Helmi), instructions" + icon: mdiArrowRight + - link: "https://qx.vtt.fi/docs/devices/q5.html" + teaser: "Read more about VTT Q5 (VTT website)" + icon: mdiOpenInNew supercomputer_resources: - name: LUMI @@ -234,11 +241,8 @@ cookie_consent: Additionally, users should also acknowledge using FiQCI quantum computers if applicable: - helmi-link-url: /publications/2022-11-01-Helmi_pilot - helmi-link-text: VTT Q5 (Helmi) - - q50-link-url: /publications/2025-03-04-Q50-Call-2_2025 - q50-link-text: VTT Q50 + acknowledge-link-url: /publications/2026-05-11-Academic-Call-2026-02 + acknowledge-link-text: see here "/status/": info: Here you can find the status of the Quantum Computers connected to LUMI. @@ -249,23 +253,23 @@ cookie_consent: href: https://www.lumi-supercomputer.eu/lumi-service-status/ alerts: # Each alert renders as its own banner. type can be info, warning, or error - text: |- - VTT Q5 (Helmi) will be indefinitely offline and will be removed from our services soon. - type: warning + Aalto Q20 is now available for use. + type: info - text: |- VTT Q50 will be down for maintenance from 19th June to 17th July. During this time, the Q50 will not be available for use. We apologize for any inconvenience this may cause. type: warning quantum-computers: - - name: VTT Q5 (Helmi) - qubits: 5 - basis: "PRX, CZ" - topology: "Star" - device_id: "Q5" - - name: VTT Q50 qubits: 53 basis: "PRX, CZ" topology: "Square grid" device_id: "Q50" + + - name: Aalto Q20 + qubits: 20 + basis: "PRX, CZ" + topology: "Square grid" + device_id: "Q20" "/cookies/": title: Cookie Policy diff --git a/content/assets/images/aalto-qc/aalto-q20.webp b/content/assets/images/aalto-qc/aalto-q20.webp new file mode 100644 index 000000000000..281efa6df1f1 Binary files /dev/null and b/content/assets/images/aalto-qc/aalto-q20.webp differ diff --git a/content/pages/about.md b/content/pages/about.md index 567d28713c92..af441b331940 100644 --- a/content/pages/about.md +++ b/content/pages/about.md @@ -35,11 +35,8 @@ react: true

Acknowledgement

{{ about_data.advisory-group.acknowledgement.desc }} - - {{ about_data.advisory-group.acknowledgement.helmi-link-text }} - , - - {{ about_data.advisory-group.acknowledgement.q50-link-text }} + + {{ about_data.advisory-group.acknowledgement.acknowledge-link-text }}

diff --git a/docs/blog_post.md b/docs/blog_post.md index 27bd8aaed80e..bba94d89235d 100644 --- a/docs/blog_post.md +++ b/docs/blog_post.md @@ -38,7 +38,7 @@ header: #thumbnail image for the post published: true author: author name #name of the author layout: post #don't change -tags: #keywords related to the topic of the blog, e.g Helmi, Quantum, etc +tags: #keywords related to the topic of the blog, e.g Q20, Quantum, etc - tag_1 - tag_2 - etc diff --git a/docs/edit_or_add_content.md b/docs/edit_or_add_content.md index c5cf86fa3755..16b4eab44835 100644 --- a/docs/edit_or_add_content.md +++ b/docs/edit_or_add_content.md @@ -41,11 +41,11 @@ Add the quantum computer to the lists at: ```yml "/status/": quantum-computers: - - name: VTT Q5 (Helmi) - qubits: 5 + - name: Aalto Q20 + qubits: 20 basis: "PRX, CZ" - topology: "Star" - device_id: "Q5" + topology: "Grid" + device_id: "Q20" ... ``` @@ -53,20 +53,17 @@ and ```yml "/access/": - quantum_resources: #list of the available quantum resources - - name: VTT Q5 (Helmi) + quantum_resources: + - name: Aalto Q20 desc: |- - The VTT Q5 "Helmi" is the first Finnish quantum computer. Helmi is based on superconducting technology and provides 5 qubits - in a star-shape topology. Hybrid HPC+QC access was opened in 2022. Helmi is operated by VTT, - and was co-developed with IQM Quantm Computers. + Aalto Q20 is an IQM superconducting quantum computer with 20 qubits in a square grid topology. It is operated by Aalto University. image: "/assets/images/vtt-images/VTT_lab-2.webp" links: - - link: "" - teaser: "How to access VTT Q5, instructions" + - link: "/publications/2026-05-11-Academic-Call-2026-02" + teaser: "Apply for access to Aalto Q20" + - link: "https://docs.csc.fi/computing/quantum-computing/running-quantum-jobs/" + teaser: "How to access Aalto Q20, instructions" icon: mdiArrowRight - - link: "" - teaser: "Read more about VTT Q5 (VTT website)" - icon: mdiOpenInNew ... ``` @@ -281,7 +278,7 @@ Each section contains static text content, link urls, image paths, etc else that description: |- The Finnish quantum-computing infrastructure (FiQCI) provides access to quantum computers through the LUMI supercomputer environment. Currently there are two quantum computers - available for use: VTT Q5 (Helmi), and VTT Q50 + available for use: Aalto Q20, and VTT Q50 links: - title: Quantum computer resources href: /access/#quantum @@ -317,20 +314,18 @@ Each section contains static text content, link urls, image paths, etc else that title: FiQCI QPU resource estimator href: https://fiqci.fi/resource-estimator - quantum_resources: #list of the available quantum resources - - name: VTT Q5 (Helmi) + quantum_resources: + - name: Aalto Q20 desc: |- - The VTT Q5 "Helmi" is the first Finnish quantum computer. Helmi is based on superconducting technology and provides 5 qubits - in a star-shape topology. Hybrid HPC+QC access was opened in 2022. Helmi is operated by VTT, - and was co-developed with IQM Quantm Computers. + Aalto Q20 is an IQM superconducting quantum computer with 20 qubits in a square grid topology. It is operated by Aalto University. image: "/assets/images/vtt-images/VTT_lab-2.webp" links: - - link: "" - teaser: "How to access VTT Q5, instructions" + - link: "/publications/2026-05-11-Academic-Call-2026-02" + teaser: "Apply for access to Aalto Q20" + - link: "https://docs.csc.fi/computing/quantum-computing/running-quantum-jobs/" + teaser: "How to access Aalto Q20, instructions" icon: mdiArrowRight - - link: "" - teaser: "Read more about VTT Q5 (VTT website)" - icon: mdiOpenInNew + - name: VTT Q50 desc: |- @@ -432,8 +427,7 @@ Each section contains static text content, link urls, image paths, etc else that institution: Aalto University title: FiQCI vice-director - acknowledgement: #info for how to acknowledge results - #obtained using FiQCI + acknowledgement: desc: |- When publishing the results that utilise the FiQCI infrastructure, users should acknowledge the use of FiQCI, preferably in the format: "These [results] have been acquired using the @@ -441,11 +435,8 @@ Each section contains static text content, link urls, image paths, etc else that Additionally, users should also acknowledge using FiQCI quantum computers if applicable: - helmi-link-url: /publications/2022-11-01-Helmi_pilot - helmi-link-text: VTT Q5 (Helmi) - - q50-link-url: /publications/2025-07-03-Q50-Call-2_2025 - q50-link-text: VTT Q50 + acknowledge-link-url: /publications/2026-05-11-Academic-Call-2026-02 + acknowledge-link-text: VTT Q50 ``` ## Status @@ -461,18 +452,18 @@ Each section contains static text content, link urls, image paths, etc else that alert: text: Note! Access to VTT Q50 through FiQCI is coming soon. Status of the quantum computer can already be seen below. type: warning # Alert type can be info, warning, or error - quantum-computers: - - name: VTT Q5 (Helmi) - qubits: 5 - basis: "PRX, CZ" - topology: "Star" - device_id: "Q5" - + quantum-computers: - name: VTT Q50 - qubits: 54 + qubits: 53 basis: "PRX, CZ" topology: "Square grid" device_id: "Q50" + + - name: Aalto Q20 + qubits: 20 + basis: "PRX, CZ" + topology: "Square grid" + device_id: "Q20" ``` ## Cookies diff --git a/docs/event.md b/docs/event.md index 84c0a5b31d4e..ba6ba65c460f 100644 --- a/docs/event.md +++ b/docs/event.md @@ -25,7 +25,7 @@ title: 'Title of the Event' #Event name date: yyyy-mm-dd #date when to be published as yyyy-mm-dd published: true #leave as is link: https://the-event.fi/info/signup #link to event page -tags: #keywords related to the event, e.g Helmi, Quantum, etc +tags: #keywords related to the event, e.g Q20, Quantum, etc - tag_1 - tag_2 - etc diff --git a/package-lock.json b/package-lock.json index 215cf5c6e44e..635bf7335246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "katex": "^0.16.21", "lunr": "^2.3.9", "prismjs": "^1.30.0", - "react-calendar": "^6.0.0" + "react-calendar": "^6.0.0", + "react-zoom-pan-pinch": "^4.0.3" }, "devDependencies": { "@babel/core": "^7.26.0", @@ -3659,6 +3660,21 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5082,6 +5098,20 @@ "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==", "license": "MIT" }, + "node_modules/react-zoom-pan-pinch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-4.0.3.tgz", + "integrity": "sha512-N2Hi6L78fFmhRra+ORpFSW7WST5x6kxpOPplIvtB0b7b+U2anpo1z1wLgaWRPS2kUSqcraRG+JgBCIlDJnqqAg==", + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 0fe90016a244..5b0045266fc9 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,8 @@ "katex": "^0.16.21", "lunr": "^2.3.9", "prismjs": "^1.30.0", - "react-calendar": "^6.0.0" + "react-calendar": "^6.0.0", + "react-zoom-pan-pinch": "^4.0.3" }, "private": "true" } diff --git a/src/components/QcLayouts/Base.jsx b/src/components/QcLayouts/Base.jsx deleted file mode 100644 index 3bac0e6c31f0..000000000000 --- a/src/components/QcLayouts/Base.jsx +++ /dev/null @@ -1,437 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { motion } from 'framer-motion'; -import { getColorForMetricValue } from '../../utils/generateGradient'; -import { formatMetricValue } from '../../utils/formatMetricValue'; - -export function BaseQcLayout({ rawNodes, edges, spacing, calibrationData, qubitMetric, couplerMetric, - qubitMetricFormatted, couplerMetricFormatted, thresholdQubit, thresholdCoupler }) { - - const [hoveredNode, setHoveredNode] = useState(null); - const [hoveredEdge, setHoveredEdge] = useState(null); - const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); - const [tooltip, setTooltip] = useState(null); - const containerRef = useRef(null); - - - // Calculate metric statistics for qubit metrics - const getQubitMetricStats = () => { - if (!calibrationData || !qubitMetric || !calibrationData[qubitMetric]) { - return null; - } - - const values = Object.values(calibrationData[qubitMetric]) - .map(data => data?.value) - .filter(value => value !== null && value !== undefined); - - if (values.length === 0) return null; - - const sortedValues = [...values].sort((a, b) => a - b); - const worst = sortedValues[0]; - const best = sortedValues[sortedValues.length - 1]; - const average = calibrationData[qubitMetric].statistics.median; - - return { worst, best, average }; - }; - - // Calculate metric statistics for coupler metrics - const getCouplerMetricStats = () => { - if (!calibrationData || !couplerMetric || !calibrationData[couplerMetric]) { - return null; - } - - const values = Object.values(calibrationData[couplerMetric]) - .map(data => data?.value) - .filter(value => value !== null && value !== undefined); - - if (values.length === 0) return null; - - const sortedValues = [...values].sort((a, b) => a - b); - const worst = sortedValues[0]; - const best = sortedValues[sortedValues.length - 1]; - const average = calibrationData[couplerMetric].statistics.median; - - return { worst, best, average }; - }; - - // Get metric value for a specific qubit - const getQubitMetricValue = (qubitId) => { - if (!calibrationData || !qubitMetric || !calibrationData[qubitMetric]) { - return null; - } - - const metricData = calibrationData[qubitMetric][qubitId]; - return metricData?.value ?? null; - }; - - // Get unit for the current metric - const getMetricUnit = () => { - if (!calibrationData || !qubitMetric || !calibrationData[qubitMetric]) { - return ''; - } - - // Try to get unit from first available qubit - const firstQubitData = Object.values(calibrationData[qubitMetric]).find(data => data?.unit); - return firstQubitData?.unit || ''; - }; - - // Get color for a specific qubit - const getQubitColor = (qubitId) => { - // If no metric is selected, return default blue - if (!qubitMetric || qubitMetric == '') { - return '#004E84'; - } - // If qubit does not exist in calibration data, return grey - if (!calibrationData || !calibrationData[qubitMetric] || !(qubitId in calibrationData[qubitMetric])) { - return '#888888'; - } - - const value = getQubitMetricValue(qubitId); - if (value === null || value === undefined) { - return '#888888'; - } - - const stats = getQubitMetricStats(); - if (!stats) { - return '#888888'; - } - - // For metrics where lower values are better (like error rates, times) - const unit = getMetricUnit(); - let worst = stats.worst; - let best = stats.best; - // Reverse the scale for certain metrics where lower is better - if ((qubitMetric && qubitMetric.includes("error"))) { - worst = stats.best; - best = stats.worst; - if (parseFloat(value) > thresholdQubit) { - return '#888888'; // Grey if above threshold (higher error is worse) - } - } - else { - if (parseFloat(value) < thresholdQubit) { - return '#888888'; // Grey if below threshold (lower value is worse) - } - } - - return getColorForMetricValue(value, worst, best, stats.average); - }; - - // Get coupler metric value for a specific coupler - const getCouplerMetricValue = (coupleId) => { - if (!calibrationData || !couplerMetric || !calibrationData[couplerMetric]) { - return null; - } - - const metricData = calibrationData[couplerMetric][coupleId]; - return metricData?.value ?? null; - }; - - // Get unit for the current coupler metric - const getCouplerMetricUnit = () => { - if (!calibrationData || !couplerMetric || !calibrationData[couplerMetric]) { - return ''; - } - - // Try to get unit from first available coupler - const firstCouplerData = Object.values(calibrationData[couplerMetric]).find(data => data?.unit); - return firstCouplerData?.unit || ''; - }; - - // Get color for a specific coupler/edge - const getCouplerColor = (nodeA, nodeB) => { - // If no metric is selected, return default grey - if (!couplerMetric || couplerMetric == '') { - return '#aaa'; - } - - // Create coupler key - const coupleId1 = `${nodeA}__${nodeB}`; - const coupleId2 = `${nodeB}__${nodeA}`; - - // If coupler does not exist in calibration data, return grey - if (!calibrationData || !calibrationData[couplerMetric]) { - return '#888888'; - } - - let value = getCouplerMetricValue(coupleId1); - if (value === null) { - value = getCouplerMetricValue(coupleId2); - } - if (value === null || value === undefined) { - return '#888888'; // Grey if no data found - } - - const stats = getCouplerMetricStats(); - if (!stats) { - return '#888888'; - } - - // For metrics where lower values are better - const unit = getCouplerMetricUnit(); - let worst = stats.worst; - let best = stats.best; - - // Reverse the scale for certain metrics where lower is better - if ((couplerMetric && couplerMetric.includes("error"))) { - worst = stats.best; - best = stats.worst; - - if (parseFloat(value) > thresholdCoupler) { - return '#888888'; // Grey if above threshold (higher error is worse) - } - } - else { - if (parseFloat(value) < thresholdCoupler) { - return '#888888'; // Grey if below threshold (lower value is worse) - } - } - - return getColorForMetricValue(value, worst, best, stats.average); - }; - - // Handle mouse move to update tooltip position relative to the container - const handleMouseMove = (event) => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - - setMousePos({ - x: event.clientX - rect.left, - y: event.clientY - rect.top - }); - } - }; - - // Calculate tooltip position with boundary checking - const getTooltipPosition = () => { - 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 - - if (qubitMetric === '' && couplerMetric === '') { - tooltipWidth = 100; // rough estimate - 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 - } - - // Check bottom boundary against viewport - if (absoluteY + 8 + tooltipHeight > viewportHeight) { - top = mousePos.y - tooltipHeight - 8; // Position above 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)); - - return { left, top }; - }; - - // Build coordinate map directly from raw nodes - const coordMap = Object.fromEntries(rawNodes.map(n => [n.id, n])); - - // Compute dynamic bounds - const xs = rawNodes.map(n => n.x); - const ys = rawNodes.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; - const viewBoxWidth = maxX - minX; - const viewBoxHeight = maxY - minY; - const viewBox = `${minX} ${-maxY} ${viewBoxWidth} ${viewBoxHeight}`; - - // Calculate max width based on number of qubits - const maxWidth = rawNodes.length * 100; - - return ( -
{ setHoveredNode(null); setHoveredEdge(null); setTooltip(null); }} - > - - {edges.map(([a, b]) => { - const A = coordMap[a]; - const B = coordMap[b]; - const key = `${a}-${b}`; - const hover = hoveredEdge === key; - const edgeColor = getCouplerColor(a, b); - return ( - { - const coupleId1 = `${a}__${b}`; - const coupleId2 = `${b}__${a}`; - const metricValue = getCouplerMetricValue(coupleId1) ?? getCouplerMetricValue(coupleId2); - const unit = getCouplerMetricUnit(); - const formattedValue = formatMetricValue(metricValue, unit); - const metricInfo = metricValue !== null && couplerMetricFormatted ? `${couplerMetricFormatted}: ${formattedValue}` : ''; - setHoveredEdge(key); - let uncertainty = null; - const coupleId = getCouplerMetricValue(coupleId1) !== null ? coupleId1 : coupleId2; - if ( - calibrationData && - couplerMetric && - calibrationData[couplerMetric] && - calibrationData[couplerMetric][coupleId] && - calibrationData[couplerMetric][coupleId].uncertainty !== undefined - ) { - uncertainty = calibrationData[couplerMetric][coupleId].uncertainty; - } - let uncertaintyInfo = (uncertainty !== null && uncertainty !== "None") ? ` (±${formatMetricValue(uncertainty, unit)})` : ''; - let details; - if (couplerMetric && couplerMetric !== '' && metricValue === null) { - details = `Disabled`; - } else { - details = `${metricInfo}${uncertaintyInfo}`; - } - setTooltip({ - type: 'edge', - content: `Coupler: ${a}__${b}`, - details - }); - }} - onMouseLeave={() => { setHoveredEdge(null); setTooltip(null); }} - className={hover ? 'cursor-pointer filter drop-shadow-md' : 'cursor-pointer'} - /> - ); - })} - {rawNodes.map(n => { - const hover = hoveredNode === n.id; - const nodeColor = getQubitColor(n.id); - return ( - - { - const metricValue = getQubitMetricValue(n.id); - const unit = getMetricUnit(); - const formattedValue = formatMetricValue(metricValue, unit); - const metricInfo = metricValue !== null ? `${qubitMetricFormatted}: ${formattedValue}` : ''; - setHoveredNode(n.id); - let uncertainty = null; - if ( - calibrationData && - qubitMetric && - calibrationData[qubitMetric] && - calibrationData[qubitMetric][n.id] && - calibrationData[qubitMetric][n.id].uncertainty !== undefined - ) { - uncertainty = calibrationData[qubitMetric][n.id].uncertainty; - } - let uncertaintyInfo = (uncertainty !== null && uncertainty !== "None") ? ` (±${formatMetricValue(uncertainty, unit)})` : ''; - let details; - if (qubitMetric && qubitMetric !== '' && metricValue === null) { - details = `Disabled`; - } else { - details = `${metricInfo}${uncertaintyInfo}`; - } - setTooltip({ - type: 'node', - content: `Qubit: ${n.id}`, - details - }); - }} - onMouseLeave={() => { setHoveredNode(null); setTooltip(null); }} - className={hover ? 'cursor-pointer filter drop-shadow-lg' : 'cursor-pointer'} - > - - - {n.id} - - - - ); - })} - - - {tooltip && ( - -
- {/* Arrow pointing to the element */} -
- - {/* Header with icon */} -
-
-
- {tooltip.content} -
-
- - {/* Details with better formatting */} - {tooltip.details && -
-

- {tooltip.details.split(":")[0]} - {tooltip.details !== "Disabled" && - : - } -

- -

- {tooltip.details.split(":")[1]} -

-
- } - - - {/* Bottom accent */} -
-
-
- )} -
- ); -} diff --git a/src/components/QcLayouts/Helmi.jsx b/src/components/QcLayouts/Helmi.jsx deleted file mode 100644 index cfc7abdfd087..000000000000 --- a/src/components/QcLayouts/Helmi.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { BaseQcLayout } from './Base'; - -const spacing = 100; - -const rawNodes = [ - { id: 'QB5', x: -200 + 20, y: 0 + 20 }, - { id: 'QB4', x: -360 - 20, y: -160 - 20 }, - { id: 'QB3', x: -280, y: -80 }, // QB2 stays in place - { id: 'QB2', x: -360 - 20, y: 0 + 20 }, - { id: 'QB1', x: -200 + 20, y: -160 - 20 }, -]; -const edges = [ - ['QB3', 'QB5'], - ['QB3', 'QB4'], - ['QB2', 'QB3'], - ['QB1', 'QB3'], -]; - -export const HelmiLayout = ({ calibrationData, qubitMetric, couplerMetric, - qubitMetricFormatted, couplerMetricFormatted, thresholdQubit, thresholdCoupler }) => { - return ( - - ) -} \ No newline at end of file diff --git a/src/components/QcLayouts/Q50.jsx b/src/components/QcLayouts/Q50.jsx deleted file mode 100644 index a6a9492360a1..000000000000 --- a/src/components/QcLayouts/Q50.jsx +++ /dev/null @@ -1,176 +0,0 @@ -import React from 'react'; -import { BaseQcLayout } from './Base'; - -const spacing = 100; -const xOrigin = -210; -const yOrigin = 440; - -const rawNodes = [ - // Top row (3 qubit) - { id: 'QB54', x: xOrigin + spacing * 0, y: yOrigin + spacing * 0 }, - { id: 'QB53', x: xOrigin + spacing * -1, y: yOrigin + spacing * -1 }, - { id: 'QB52', x: xOrigin + spacing * -2, y: yOrigin + spacing * -2 }, - - // Row 2 (5 qubits) - { id: 'QB51', x: xOrigin + spacing * 2, y: yOrigin + spacing * 0 }, - { id: 'QB50', x: xOrigin + spacing * 1, y: yOrigin + spacing * -1 }, - { id: 'QB49', x: xOrigin + spacing * 0, y: yOrigin + spacing * -2 }, - { id: 'QB48', x: xOrigin + spacing * -1, y: yOrigin + spacing * -3 }, - { id: 'QB47', x: xOrigin + spacing * -2, y: yOrigin + spacing * -4 }, - - // Row 3 (7 qubits) - { id: 'QB46', x: xOrigin + spacing * 4, y: yOrigin + spacing * 0 }, - { id: 'QB45', x: xOrigin + spacing * 3, y: yOrigin + spacing * -1 }, - { id: 'QB44', x: xOrigin + spacing * 2, y: yOrigin + spacing * -2 }, - { id: 'QB43', x: xOrigin + spacing * 1, y: yOrigin + spacing * -3 }, - { id: 'QB42', x: xOrigin + spacing * 0, y: yOrigin + spacing * -4 }, - { id: 'QB41', x: xOrigin + spacing * -1, y: yOrigin + spacing * -5 }, - { id: 'QB40', x: xOrigin + spacing * -2, y: yOrigin + spacing * -6 }, - - // Row 4 (9 qubits) - { id: 'QB39', x: xOrigin + spacing * 6, y: yOrigin + spacing * 0 }, - { id: 'QB38', x: xOrigin + spacing * 5, y: yOrigin + spacing * -1 }, - { id: 'QB37', x: xOrigin + spacing * 4, y: yOrigin + spacing * -2 }, - { id: 'QB36', x: xOrigin + spacing * 3, y: yOrigin + spacing * -3 }, - { id: 'QB35', x: xOrigin + spacing * 2, y: yOrigin + spacing * -4 }, - { id: 'QB34', x: xOrigin + spacing * 1, y: yOrigin + spacing * -5 }, - { id: 'QB33', x: xOrigin + spacing * 0, y: yOrigin + spacing * -6 }, - { id: 'QB32', x: xOrigin + spacing * -1, y: yOrigin + spacing * -7 }, - - // Row 5 (9 qubits) - { id: 'QB31', x: xOrigin + spacing * 7, y: yOrigin + spacing * -1 }, - { id: 'QB30', x: xOrigin + spacing * 6, y: yOrigin + spacing * -2 }, - { id: 'QB29', x: xOrigin + spacing * 5, y: yOrigin + spacing * -3 }, - { id: 'QB28', x: xOrigin + spacing * 4, y: yOrigin + spacing * -4 }, - { id: 'QB27', x: xOrigin + spacing * 3, y: yOrigin + spacing * -5 }, - { id: 'QB26', x: xOrigin + spacing * 2, y: yOrigin + spacing * -6 }, - { id: 'QB25', x: xOrigin + spacing * 1, y: yOrigin + spacing * -7 }, - { id: 'QB24', x: xOrigin + spacing * 0, y: yOrigin + spacing * -8 }, - { id: 'QB23', x: xOrigin + spacing * -1, y: yOrigin + spacing * -9 }, - - // Row 6 (8 qubits) - { id: 'QB22', x: xOrigin + spacing * 7, y: yOrigin + spacing * -3 }, - { id: 'QB21', x: xOrigin + spacing * 6, y: yOrigin + spacing * -4 }, - { id: 'QB20', x: xOrigin + spacing * 5, y: yOrigin + spacing * -5 }, - { id: 'QB19', x: xOrigin + spacing * 4, y: yOrigin + spacing * -6 }, - { id: 'QB18', x: xOrigin + spacing * 3, y: yOrigin + spacing * -7 }, - { id: 'QB17', x: xOrigin + spacing * 2, y: yOrigin + spacing * -8 }, - { id: 'QB16', x: xOrigin + spacing * 1, y: yOrigin + spacing * -9 }, - { id: 'QB15', x: xOrigin + spacing * 0, y: yOrigin + spacing * -10 }, - - // Row 7 (7 qubits) - { id: 'QB14', x: xOrigin + spacing * 8, y: yOrigin + spacing * -4 }, - { id: 'QB13', x: xOrigin + spacing * 7, y: yOrigin + spacing * -5 }, - { id: 'QB12', x: xOrigin + spacing * 6, y: yOrigin + spacing * -6 }, - { id: 'QB11', x: xOrigin + spacing * 5, y: yOrigin + spacing * -7 }, - { id: 'QB10', x: xOrigin + spacing * 4, y: yOrigin + spacing * -8 }, - { id: 'QB9', x: xOrigin + spacing * 3, y: yOrigin + spacing * -9 }, - { id: 'QB8', x: xOrigin + spacing * 2, y: yOrigin + spacing * -10 }, - - // Row 8 (6 qubits) - { id: 'QB7', x: xOrigin + spacing * 8, y: yOrigin + spacing * -6 }, - { id: 'QB6', x: xOrigin + spacing * 7, y: yOrigin + spacing * -7 }, - { id: 'QB5', x: xOrigin + spacing * 6, y: yOrigin + spacing * -8 }, - { id: 'QB4', x: xOrigin + spacing * 5, y: yOrigin + spacing * -9 }, - { id: 'QB3', x: xOrigin + spacing * 4, y: yOrigin + spacing * -10 }, - - // Row 9 (2 qubits) - { id: 'QB2', x: xOrigin + spacing * 8, y: yOrigin + spacing * -8 }, - { id: 'QB1', x: xOrigin + spacing * 7, y: yOrigin + spacing * -9 }, - - -]; - -// Define edges based on nearest neighbor connectivity in the diamond pattern -const edges = [ - // Row 1 connections - ['QB54', 'QB53'], ['QB53', 'QB52'], ['QB54', 'QB50'], ['QB53', 'QB49'], ['QB52', 'QB48'], - - // Row 2 connections - ['QB51', 'QB50'], ['QB50', 'QB49'], - ['QB49', 'QB48'], ['QB48', 'QB47'], - - ['QB51', 'QB45'], ['QB50', 'QB44'], - ['QB49', 'QB43'], ['QB48', 'QB42'], ['QB47', 'QB41'], - - // Row 3 connections - ['QB46', 'QB45'], ['QB45', 'QB44'], - ['QB44', 'QB43'], ['QB43', 'QB42'], - ['QB42', 'QB41'], ['QB41', 'QB40'], - - ['QB46', 'QB38'], ['QB45', 'QB37'], - ['QB44', 'QB36'], ['QB43', 'QB35'], - ['QB42', 'QB34'], ['QB41', 'QB33'], ['QB40', 'QB32'], - - - // Row 4 connections - ['QB39', 'QB38'], ['QB38', 'QB37'], - ['QB37', 'QB36'], ['QB36', 'QB35'], - ['QB35', 'QB34'], ['QB34', 'QB33'], - ['QB33', 'QB32'], - - ['QB39', 'QB31'], ['QB38', 'QB30'], - ['QB37', 'QB29'], ['QB36', 'QB28'], - ['QB35', 'QB27'], ['QB34', 'QB26'], - ['QB33', 'QB25'], ['QB32', 'QB24'], - - // Row 5 connections - ['QB31', 'QB30'], ['QB30', 'QB29'], - ['QB29', 'QB28'], ['QB28', 'QB27'], - ['QB27', 'QB26'], ['QB26', 'QB25'], - ['QB25', 'QB24'], ['QB24', 'QB23'], - - ['QB30', 'QB22'], - ['QB29', 'QB21'], ['QB28', 'QB20'], - ['QB27', 'QB19'], ['QB26', 'QB18'], - ['QB25', 'QB17'], ['QB24', 'QB16'], - ['QB23', 'QB15'], - - // Row 6 connections - ['QB22', 'QB21'], ['QB21', 'QB20'], - ['QB20', 'QB19'], ['QB19', 'QB18'], - ['QB18', 'QB17'], ['QB17', 'QB16'], - ['QB16', 'QB15'], - - ['QB22', 'QB14'], ['QB21', 'QB13'], - ['QB20', 'QB12'], ['QB19', 'QB11'], - ['QB18', 'QB10'], ['QB17', 'QB9'], - ['QB16', 'QB8'], - - // Row 7 connections - ['QB14', 'QB13'], ['QB13', 'QB12'], - ['QB12', 'QB11'], ['QB11', 'QB10'], - ['QB10', 'QB9'], ['QB9', 'QB8'], - - ['QB13', 'QB7'], ['QB12', 'QB6'], - ['QB11', 'QB5'], ['QB10', 'QB4'], - ['QB9', 'QB3'], - - - // Row 8 connections - ['QB7', 'QB6'], ['QB6', 'QB5'], ['QB5', 'QB4'], - ['QB4', 'QB3'], - - ['QB6', 'QB2'], ['QB5', 'QB1'], - - // Row 9 connections - ['QB2', 'QB1'] -]; - -export const Q50Layout = ({ calibrationData, qubitMetric, couplerMetric, - qubitMetricFormatted, couplerMetricFormatted, thresholdQubit, thresholdCoupler }) => { - return ( - - ) -} \ No newline at end of file diff --git a/src/components/QcLayouts/QcLayout.jsx b/src/components/QcLayouts/QcLayout.jsx new file mode 100644 index 000000000000..8e1cb93c56a3 --- /dev/null +++ b/src/components/QcLayouts/QcLayout.jsx @@ -0,0 +1,321 @@ +import React, { useState, useRef } from 'react'; +import { motion } from 'framer-motion'; +import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'; +import { getColorForMetricValue } from '../../utils/generateGradient'; +import { formatMetricValue } from '../../utils/formatMetricValue'; +import { getMetricStatistics } from '../../utils/sidebarUtils'; + +const DEFAULT_NODE_COLOR = '#004E84'; +const GREY = '#888888'; + +// Node rect side length in SVG user units (must match the below). +const NODE_UNITS = 90; +// Largest a qubit node may render on screen, in pixels. The whole layout is +// capped proportionally so nodes/couplers never exceed this, regardless of how +// few qubits a layout has (otherwise small layouts like Q20 scale up huge). +const MAX_NODE_PX = 48; + +export function QcLayout({ layout, metrics }) { + const { spacing, nodes, edges } = layout; + const { + calibrationData, qubitMetric, couplerMetric, + qubitMetricFormatted, couplerMetricFormatted, thresholdQubit, thresholdCoupler, + } = metrics; + + const [hoveredNode, setHoveredNode] = useState(null); + const [hoveredEdge, setHoveredEdge] = useState(null); + const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); + const [tooltip, setTooltip] = useState(null); + const containerRef = useRef(null); + + // Get the metric value for a given id (qubit id or coupler key) + const getMetricValue = (metric, id) => { + if (!calibrationData || !metric || !calibrationData[metric]) return null; + return calibrationData[metric][id]?.value ?? null; + }; + + // Get the unit for a metric, read from the first entry that has one + const getMetricUnit = (metric) => { + if (!calibrationData || !metric || !calibrationData[metric]) return ''; + const entry = Object.values(calibrationData[metric]).find(data => data?.unit); + return entry?.unit || ''; + }; + + // 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}`; + }; + + // Color for a metric value. dim: 1 = qubit, 2 = coupler. + 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'; + if (!calibrationData || !calibrationData[metric]) return GREY; + + const value = getMetricValue(metric, id); + if (value === null || value === undefined) return GREY; + + const stats = getMetricStatistics(calibrationData, metric, dim); + if (!stats) return GREY; + + let worst = stats.worst; + let best = stats.best; + // Reverse the scale for metrics where lower is better (error rates) + if (metric.includes('error')) { + worst = stats.best; + best = stats.worst; + if (parseFloat(value) > threshold) return GREY; // above threshold = worse + } else if (parseFloat(value) < threshold) { + return GREY; // below threshold = worse + } + + return getColorForMetricValue(value, worst, best, stats.average); + }; + + // Build tooltip content for a hovered qubit or coupler. + // label is the displayed title (e.g. "Qubit: QB1" / "Coupler: QB1__QB2"). + const buildTooltip = (type, metric, id, formatted, label) => { + const value = getMetricValue(metric, id); + const unit = getMetricUnit(metric); + const metricInfo = value !== null && formatted ? `${formatted}: ${formatMetricValue(value, unit)}` : ''; + + const uncertainty = calibrationData?.[metric]?.[id]?.uncertainty; + const uncertaintyInfo = (uncertainty !== undefined && uncertainty !== null && uncertainty !== 'None') + ? ` (±${formatMetricValue(uncertainty, unit)})` + : ''; + + const details = (metric && metric !== '' && value === null) + ? 'Disabled' + : `${metricInfo}${uncertaintyInfo}`; + + return { type, content: label, details }; + }; + + // Handle mouse move to update tooltip position relative to the container + const handleMouseMove = (event) => { + if (containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + setMousePos({ + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }); + } + }; + + // Calculate tooltip position with boundary checking + const getTooltipPosition = () => { + 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 + + if (qubitMetric === '' && couplerMetric === '') { + tooltipWidth = 100; // rough estimate + 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 + } + + // Check bottom boundary against viewport + if (absoluteY + 8 + tooltipHeight > viewportHeight) { + top = mousePos.y - tooltipHeight - 8; // Position above 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)); + + return { left, top }; + }; + + // Build coordinate map directly from nodes + const coordMap = Object.fromEntries(nodes.map(n => [n.id, n])); + + // Compute dynamic bounds + 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; + 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); + + return ( +
{ setHoveredNode(null); setHoveredEdge(null); setTooltip(null); }} + > + setTooltip(null)} + > + {({ zoomIn, zoomOut, resetTransform }) => ( + <> +
+ + + +
+ + + {edges.map(([a, b]) => { + const A = coordMap[a]; + const B = coordMap[b]; + const key = `${a}-${b}`; + const hover = hoveredEdge === key; + const edgeColor = getColor(couplerMetric, couplerKey(a, b), 2, thresholdCoupler); + return ( + { + setHoveredEdge(key); + setTooltip(buildTooltip('edge', couplerMetric, couplerKey(a, b), couplerMetricFormatted, `Coupler: ${a}__${b}`)); + }} + onMouseLeave={() => { setHoveredEdge(null); setTooltip(null); }} + className={hover ? 'cursor-pointer filter drop-shadow-md' : 'cursor-pointer'} + /> + ); + })} + {nodes.map(n => { + const hover = hoveredNode === n.id; + const nodeColor = getColor(qubitMetric, n.id, 1, thresholdQubit); + return ( + + { + setHoveredNode(n.id); + setTooltip(buildTooltip('node', qubitMetric, n.id, qubitMetricFormatted, `Qubit: ${n.id}`)); + }} + onMouseLeave={() => { setHoveredNode(null); setTooltip(null); }} + className={hover ? 'cursor-pointer filter drop-shadow-lg' : 'cursor-pointer'} + > + + + {n.id} + + + + ); + })} + + + + )} +
+ + {tooltip && ( + +
+ {/* Header with icon */} +
+
+
+ {tooltip.content} +
+
+ + {/* Details with better formatting */} + {tooltip.details && +
+

+ {tooltip.details.split(":")[0]} + {tooltip.details !== "Disabled" && + : + } +

+ +

+ {tooltip.details.split(":")[1]} +

+
+ } + + + {/* Bottom accent */} +
+
+
+ )} +
+ ); +} diff --git a/src/components/QcLayouts/layouts.js b/src/components/QcLayouts/layouts.js new file mode 100644 index 000000000000..37b8aeb25ddf --- /dev/null +++ b/src/components/QcLayouts/layouts.js @@ -0,0 +1,94 @@ +// Layout data for quantum computer topology maps. +// +// 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. +// +// 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. + +const grid = (xOrigin, yOrigin, spacing) => (id, col, row) => ({ + id, + x: xOrigin + spacing * col, + y: yOrigin + spacing * row, +}); + +// --- Q20 --------------------------------------------------------------------- +const q20Spacing = 100; +const q20 = grid(-210, 440, q20Spacing); +const q20Nodes = [ + // Row 1 (3 qubits) + q20('QB20', 0, 0), q20('QB19', -1, -1), q20('QB18', -2, -2), + // Row 2 (5 qubits) + q20('QB17', 2, 0), q20('QB16', 1, -1), q20('QB15', 0, -2), q20('QB14', -1, -3), q20('QB13', -2, -4), + // Row 3 (5 qubits) + q20('QB12', 3, -1), q20('QB11', 2, -2), q20('QB10', 1, -3), q20('QB9', 0, -4), q20('QB8', -1, -5), + // Row 4 (5 qubits) + q20('QB7', 4, -2), q20('QB6', 3, -3), q20('QB5', 2, -4), q20('QB4', 1, -5), q20('QB3', 0, -6), + // Row 5 (2 qubits) + q20('QB2', 3, -5), q20('QB1', 2, -6), +]; +const q20Edges = [ + ['QB1', 'QB2'], ['QB1', 'QB4'], ['QB3', 'QB4'], ['QB3', 'QB8'], ['QB4', 'QB5'], + ['QB5', 'QB2'], ['QB5', 'QB6'], ['QB5', 'QB10'], ['QB7', 'QB6'], ['QB7', 'QB12'], + ['QB9', 'QB4'], ['QB9', 'QB8'], ['QB9', 'QB10'], ['QB9', 'QB14'], ['QB11', 'QB6'], + ['QB11', 'QB10'], ['QB11', 'QB12'], ['QB11', 'QB16'], ['QB13', 'QB8'], ['QB13', 'QB14'], + ['QB15', 'QB10'], ['QB15', 'QB14'], ['QB15', 'QB16'], ['QB15', 'QB19'], ['QB17', 'QB12'], + ['QB17', 'QB16'], ['QB18', 'QB14'], ['QB18', 'QB19'], ['QB20', 'QB16'], ['QB20', 'QB19'], +]; + +// --- Q50 --------------------------------------------------------------------- +const q50Spacing = 100; +const q50 = grid(-210, 440, q50Spacing); +const q50Nodes = [ + // Row 1 (3 qubits) + q50('QB54', 0, 0), q50('QB53', -1, -1), q50('QB52', -2, -2), + // Row 2 (5 qubits) + q50('QB51', 2, 0), q50('QB50', 1, -1), q50('QB49', 0, -2), q50('QB48', -1, -3), q50('QB47', -2, -4), + // Row 3 (7 qubits) + q50('QB46', 4, 0), q50('QB45', 3, -1), q50('QB44', 2, -2), q50('QB43', 1, -3), q50('QB42', 0, -4), q50('QB41', -1, -5), q50('QB40', -2, -6), + // Row 4 (8 qubits) + q50('QB39', 6, 0), q50('QB38', 5, -1), q50('QB37', 4, -2), q50('QB36', 3, -3), q50('QB35', 2, -4), q50('QB34', 1, -5), q50('QB33', 0, -6), q50('QB32', -1, -7), + // Row 5 (9 qubits) + q50('QB31', 7, -1), q50('QB30', 6, -2), q50('QB29', 5, -3), q50('QB28', 4, -4), q50('QB27', 3, -5), q50('QB26', 2, -6), q50('QB25', 1, -7), q50('QB24', 0, -8), q50('QB23', -1, -9), + // Row 6 (8 qubits) + q50('QB22', 7, -3), q50('QB21', 6, -4), q50('QB20', 5, -5), q50('QB19', 4, -6), q50('QB18', 3, -7), q50('QB17', 2, -8), q50('QB16', 1, -9), q50('QB15', 0, -10), + // Row 7 (7 qubits) + q50('QB14', 8, -4), q50('QB13', 7, -5), q50('QB12', 6, -6), q50('QB11', 5, -7), q50('QB10', 4, -8), q50('QB9', 3, -9), q50('QB8', 2, -10), + // Row 8 (5 qubits) + q50('QB7', 8, -6), q50('QB6', 7, -7), q50('QB5', 6, -8), q50('QB4', 5, -9), q50('QB3', 4, -10), + // Row 9 (2 qubits) + q50('QB2', 8, -8), q50('QB1', 7, -9), +]; +const q50Edges = [ + // Row 1 connections + ['QB54', 'QB53'], ['QB53', 'QB52'], ['QB54', 'QB50'], ['QB53', 'QB49'], ['QB52', 'QB48'], + // Row 2 connections + ['QB51', 'QB50'], ['QB50', 'QB49'], ['QB49', 'QB48'], ['QB48', 'QB47'], + ['QB51', 'QB45'], ['QB50', 'QB44'], ['QB49', 'QB43'], ['QB48', 'QB42'], ['QB47', 'QB41'], + // Row 3 connections + ['QB46', 'QB45'], ['QB45', 'QB44'], ['QB44', 'QB43'], ['QB43', 'QB42'], ['QB42', 'QB41'], ['QB41', 'QB40'], + ['QB46', 'QB38'], ['QB45', 'QB37'], ['QB44', 'QB36'], ['QB43', 'QB35'], ['QB42', 'QB34'], ['QB41', 'QB33'], ['QB40', 'QB32'], + // Row 4 connections + ['QB39', 'QB38'], ['QB38', 'QB37'], ['QB37', 'QB36'], ['QB36', 'QB35'], ['QB35', 'QB34'], ['QB34', 'QB33'], ['QB33', 'QB32'], + ['QB39', 'QB31'], ['QB38', 'QB30'], ['QB37', 'QB29'], ['QB36', 'QB28'], ['QB35', 'QB27'], ['QB34', 'QB26'], ['QB33', 'QB25'], ['QB32', 'QB24'], + // Row 5 connections + ['QB31', 'QB30'], ['QB30', 'QB29'], ['QB29', 'QB28'], ['QB28', 'QB27'], ['QB27', 'QB26'], ['QB26', 'QB25'], ['QB25', 'QB24'], ['QB24', 'QB23'], + ['QB30', 'QB22'], ['QB29', 'QB21'], ['QB28', 'QB20'], ['QB27', 'QB19'], ['QB26', 'QB18'], ['QB25', 'QB17'], ['QB24', 'QB16'], ['QB23', 'QB15'], + // Row 6 connections + ['QB22', 'QB21'], ['QB21', 'QB20'], ['QB20', 'QB19'], ['QB19', 'QB18'], ['QB18', 'QB17'], ['QB17', 'QB16'], ['QB16', 'QB15'], + ['QB22', 'QB14'], ['QB21', 'QB13'], ['QB20', 'QB12'], ['QB19', 'QB11'], ['QB18', 'QB10'], ['QB17', 'QB9'], ['QB16', 'QB8'], + // Row 7 connections + ['QB14', 'QB13'], ['QB13', 'QB12'], ['QB12', 'QB11'], ['QB11', 'QB10'], ['QB10', 'QB9'], ['QB9', 'QB8'], + ['QB13', 'QB7'], ['QB12', 'QB6'], ['QB11', 'QB5'], ['QB10', 'QB4'], ['QB9', 'QB3'], + // Row 8 connections + ['QB7', 'QB6'], ['QB6', 'QB5'], ['QB5', 'QB4'], ['QB4', 'QB3'], + ['QB6', 'QB2'], ['QB5', 'QB1'], + // Row 9 connections + ['QB2', 'QB1'], +]; + +export const QC_LAYOUTS = { + q20: { spacing: q20Spacing, nodes: q20Nodes, edges: q20Edges }, + q50: { spacing: q50Spacing, nodes: q50Nodes, edges: q50Edges }, +}; diff --git a/src/components/ServiceStatus.jsx b/src/components/ServiceStatus.jsx index a670cb1f7512..c19fdc7f0822 100644 --- a/src/components/ServiceStatus.jsx +++ b/src/components/ServiceStatus.jsx @@ -6,6 +6,7 @@ import { mdiInformation, mdiClose, mdiAlert } from '@mdi/js'; import { CCard, CCardTitle, CCardContent, CIcon, CButton, CSelect } from '@cscfi/csc-ui-react'; import { StatusModal } from './StatusModal/StatusModal'; import { BookingModal } from './bookingCalendar.jsx'; +import { API_BASE_URL } from '../config/api'; const ALERT_STYLES = { warning: { bg: 'bg-orange-200', icon: mdiAlert }, @@ -58,8 +59,8 @@ const StatusCard = (props) => { } export const ServiceStatus = (props) => { - const { status: statusList } = useStatus("https://fiqci-backend.2.rahtiapp.fi/devices/healthcheck"); - const { bookingData: bookingData } = useBookings("https://fiqci-backend.2.rahtiapp.fi/bookings") + const { status: statusList } = useStatus(`${API_BASE_URL}/devices/healthcheck`); + const { bookingData: bookingData } = useBookings(`${API_BASE_URL}/bookings`) const qcs = props["quantum-computers"] || []; const devicesWithStatus = (qcs.length === 0 || !Array.isArray(statusList)) @@ -102,6 +103,7 @@ export const ServiceStatus = (props) => { }, []); const handleCardClick = (qc) => { + setModalProps({ ...qc, devicesWithStatus }); setModalProps({ ...qc, devicesWithStatus }); setModalOpen(true); }; @@ -159,6 +161,8 @@ export const ServiceStatus = (props) => { ))} + + {bookingModalOpen && ( @@ -168,6 +172,7 @@ export const ServiceStatus = (props) => { )} + ); } diff --git a/src/components/StatusModal/MetricSwitcher.jsx b/src/components/StatusModal/MetricSwitcher.jsx index 10b4c5da0be0..20a82db77e15 100644 --- a/src/components/StatusModal/MetricSwitcher.jsx +++ b/src/components/StatusModal/MetricSwitcher.jsx @@ -55,7 +55,7 @@ export const MetricSwitcher = (props) => {
{(metricsState.qubitMetric) && (() => { - const qubitStats = metricsState.qubitMetric ? getMetricStatistics(calibrationData, metricsState.qubitMetric) : null; + const qubitStats = metricsState.qubitMetric ? getMetricStatistics(calibrationData, metricsState.qubitMetric, 1) : null; return (
@@ -197,7 +197,7 @@ export const MetricSwitcher = (props) => {
{(metricsState.couplerMetric) && (() => { - const couplerStats = metricsState.couplerMetric ? getMetricStatistics(calibrationData, metricsState.couplerMetric) : null; + const couplerStats = metricsState.couplerMetric ? getMetricStatistics(calibrationData, metricsState.couplerMetric, 2) : null; return (
diff --git a/src/components/StatusModal/SideBar.jsx b/src/components/StatusModal/SideBar.jsx index 6a04ac102e4b..8daa3a42af31 100644 --- a/src/components/StatusModal/SideBar.jsx +++ b/src/components/StatusModal/SideBar.jsx @@ -29,7 +29,7 @@ export const SideBar = (props) => { // Calculate threshold values when dependencies change useEffect(() => { if (metricsState.qubitMetric && calibrationData) { - const qubitStats = getMetricStatistics(calibrationData, metricsState.qubitMetric); + const qubitStats = getMetricStatistics(calibrationData, metricsState.qubitMetric, 1); if (qubitStats) { const isLowerBetter = metricsState.qubitMetric.includes("error"); const worst = isLowerBetter ? qubitStats.best : qubitStats.worst; @@ -49,7 +49,7 @@ export const SideBar = (props) => { useEffect(() => { if (metricsState.couplerMetric && calibrationData) { - const couplerStats = getMetricStatistics(calibrationData, metricsState.couplerMetric); + const couplerStats = getMetricStatistics(calibrationData, metricsState.couplerMetric, 2); if (couplerStats) { const isLowerBetter = metricsState.couplerMetric.includes("error"); const worst = isLowerBetter ? couplerStats.best : couplerStats.worst; diff --git a/src/components/StatusModal/StatusModalConent.jsx b/src/components/StatusModal/StatusModalConent.jsx index d8cd514b429c..dcfdbec1fed2 100644 --- a/src/components/StatusModal/StatusModalConent.jsx +++ b/src/components/StatusModal/StatusModalConent.jsx @@ -2,8 +2,8 @@ import React from 'react' import { useState } from 'react' import { useCalibration } from '../../hooks/useCalibration'; import { useDeviceInfo } from '../../hooks/useDeviceInfo'; -import { HelmiLayout } from '../QcLayouts/Helmi'; -import { Q50Layout } from '../QcLayouts/Q50'; +import { QcLayout } from '../QcLayouts/QcLayout'; +import { QC_LAYOUTS } from '../QcLayouts/layouts'; import { Overview } from './StatusOverview'; import { CalibrationTable } from './CalibrationTable'; import { SideBar } from './SideBar'; @@ -99,27 +99,20 @@ export const ModalContent = (props) => {
- {props.device_id.toLowerCase() === 'q50' ? ( - m.value === metricsState.qubitMetric)?.name || metricsState.qubitMetric} - couplerMetricFormatted={couplerMetricOptions.find(m => m.value === metricsState.couplerMetric)?.name || metricsState.couplerMetric} - thresholdQubit={metricsState.thresholdQubitValue} - thresholdCoupler={metricsState.thresholdCouplerValue} + {QC_LAYOUTS[props.device_id.toLowerCase()] ? ( + m.value === metricsState.qubitMetric)?.name || metricsState.qubitMetric, + couplerMetricFormatted: couplerMetricOptions.find(m => m.value === metricsState.couplerMetric)?.name || metricsState.couplerMetric, + thresholdQubit: metricsState.thresholdQubitValue, + thresholdCoupler: metricsState.thresholdCouplerValue, + }} /> - ) : ( - m.value === metricsState.qubitMetric)?.name || metricsState.qubitMetric} - couplerMetricFormatted={couplerMetricOptions.find(m => m.value === metricsState.couplerMetric)?.name || metricsState.couplerMetric} - thresholdQubit={metricsState.thresholdQubitValue} - thresholdCoupler={metricsState.thresholdCouplerValue} - /> - )} + ) : null}
diff --git a/src/config/api.js b/src/config/api.js index eb4263c5bc0c..a690bbce64dd 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -1 +1 @@ -export const API_BASE_URL = "https://fiqci-backend.2.rahtiapp.fi"; \ No newline at end of file +export const API_BASE_URL = "https://fiqci-backend.2.rahtiapp.fi" //"https://fiqci-backend.2.rahtiapp.fi"; \ No newline at end of file diff --git a/src/config/deviceMetrics.js b/src/config/deviceMetrics.js index 67ed330f382b..9a9ca09818b3 100644 --- a/src/config/deviceMetrics.js +++ b/src/config/deviceMetrics.js @@ -35,6 +35,42 @@ export const DEVICE_METRICS = { ], }, + Q20: { + overview: { + single: { + singleGateFidelity: ['prx_rb_drag_crf_fidelity', 'prx_rb_fidelity'], + readoutFidelity: ['measure_fidelity_ssro_constant_fidelity', 'measure_ssro_fidelity'], + t1: ['t1_time'], + t2: ['t2_time'], + }, + coupler: { + twoQubitFidelity: ['cz_irb_crf_crf_fidelity', 'cz_irb_fidelity'], + cliffordFidelity: ['clifford_rb_uz_cz_fidelity', 'clifford_rb_fidelity'], + }, + }, + qubitOptions: [ + { name: 'T1 Time', value: 't1_time' }, + { name: 'T2 Time', value: 't2_time' }, + { name: 'T2 Echo Time', value: 't2_echo_time' }, + { name: 'PRX Gate Fidelity', value: 'prx_rb_drag_crf_fidelity' }, + { name: 'Clifford Gate Fidelity', value: 'clifford_rb_xy_fidelity' }, + { name: '1->0 Readout Error', value: 'measure_fidelity_ssro_constant_error_1_to_0' }, + { name: '0->1 Readout Error', value: 'measure_fidelity_ssro_constant_error_0_to_1' }, + { name: 'Readout Fidelity', value: 'measure_fidelity_ssro_constant_fidelity' }, + { name: '1->0 MCM Error', value: 'measure_ssro_constant_error_1_to_0', title: 'MCM = Mid Circuit Measurement' }, + { name: '0->1 MCM Error', value: 'measure_ssro_constant_error_0_to_1', title: 'MCM = Mid Circuit Measurement' }, + { name: 'MCM Fidelity', value: 'measure_ssro_constant_fidelity', title: 'MCM = Mid Circuit Measurement' }, + { name: 'QNDness Fidelity', value: 'measure_qndness_constant_fidelity', title: 'QND = Quantum Non-Demolition' }, + { name: 'QNDness 0 State', value: 'measure_qndness_constant_qndness_0', title: 'QND = Quantum Non-Demolition' }, + { name: 'QNDness 1 State', value: 'measure_qndness_constant_qndness_1', title: 'QND = Quantum Non-Demolition' }, + { name: 'QNDness Repeatability', value: 'measure_qndness_constant_repeatability', title: 'QND = Quantum Non-Demolition' }, + ], + couplerOptions: [ + { name: 'CZ Gate Fidelity', value: 'cz_irb_crf_crf_fidelity' }, + { name: 'Clifford Gate Fidelity', value: 'clifford_rb_uz_cz_fidelity' }, + ], + }, + Q50: { overview: { single: { @@ -53,7 +89,7 @@ export const DEVICE_METRICS = { { name: 'T2 Time', value: 't2_time' }, { name: 'T2 Echo Time', value: 't2_echo_time' }, { name: 'PRX Gate Fidelity', value: 'prx_rb_drag_crf_sx_fidelity' }, - { name: 'Clifford Gate Fidelity', value: 'clifford_rb_xy_fidelity' }, + { name: 'Clifford Gate Fidelity', value: 'clifford_rb_xy_sx_fidelity' }, { name: '1->0 Readout Error', value: 'measure_fidelity_ssro_constant_error_1_to_0' }, { name: '0->1 Readout Error', value: 'measure_fidelity_ssro_constant_error_0_to_1' }, { name: 'Readout Fidelity', value: 'measure_fidelity_ssro_constant_fidelity' }, diff --git a/src/utils/sidebarUtils.js b/src/utils/sidebarUtils.js index 0d36544dc93d..0464dcfdb3f1 100644 --- a/src/utils/sidebarUtils.js +++ b/src/utils/sidebarUtils.js @@ -26,24 +26,59 @@ export const downloadRawData = (data, deviceData, rawDataType) => { URL.revokeObjectURL(url); }; -export const getMetricStatistics = (calibrationData, metric) => { +export const getMetricStatistics = (calibrationData, metric, qubits = null) => { if (!calibrationData || !metric || !calibrationData[metric]) { return null; } - const values = Object.values(calibrationData[metric]) - .map(item => item?.value) - .filter(value => value !== null && value !== undefined && !isNaN(value)); + var values; + + if (qubits == 1) { + var met = calibrationData[metric]; + var filtered_met = {} + + Object.keys(met).filter(k => !k.includes("__")).map(k => filtered_met[k] = met[k]); + + values = Object.values(filtered_met).map(item => item?.value).filter(value => value !== null && value !== undefined && !isNaN(value)); + } + + else if (qubits == 2) { + var met = calibrationData[metric]; + var filtered_met = {} + + Object.keys(met).filter(k => k.includes("__")).map(k => filtered_met[k] = met[k]); + + values = Object.values(filtered_met).map(item => item?.value).filter(value => value !== null && value !== undefined && !isNaN(value)); + } + + else { + values = Object.values(calibrationData[metric]) + .map(item => item?.value) + .filter(value => value !== null && value !== undefined && !isNaN(value)); + } if (values.length === 0) return null; const sorted = values.sort((a, b) => a - b); - return { - worst: sorted[0], - best: sorted[sorted.length - 1], - average: calibrationData[metric].statistics.average, - median: calibrationData[metric].statistics.median, - unit: Object.values(calibrationData[metric]).find(item => item?.unit)?.unit || '' - }; + + + if (metric === "clifford_rb_fidelity" && qubits == 1) { + return { + worst: sorted[0], + best: sorted[sorted.length - 1], + average: sorted.reduce((sum, val) => sum + val, 0) / sorted.length, + median: sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)], + unit: Object.values(calibrationData[metric]).find(item => item?.unit)?.unit || '' + } + } + else { + return { + worst: sorted[0], + best: sorted[sorted.length - 1], + average: calibrationData[metric].statistics.average, + median: calibrationData[metric].statistics.median, + unit: Object.values(calibrationData[metric]).find(item => item?.unit)?.unit || '' + }; + } };