Skip to content

Commit 76fc97d

Browse files
authored
Add qubits validation to resourc-estimator (#25)
* Add max qubits for helmi and q50 * Update tests * Update changelog
1 parent 7e58f07 commit 76fc97d

File tree

4 files changed

+96
-12
lines changed

4 files changed

+96
-12
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Changelog
33
=========
44

5+
Version 0.3.1
6+
==========================
7+
8+
* Add device-specific qubit limits
9+
* Display maximum qubit count hint in the UI for each device
10+
511
Version 0.3.0
612
============
713

src/components/ResourceEstimator.jsx

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { DEVICE_PARAMS, calculateQPUSeconds } from '../utils/ResourceEstimatorMo
77

88
const fontFamily = '-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif';
99

10-
const ParameterInput = ({ label, value, onChange }) => {
10+
const ParameterInput = ({ label, value, onChange, hint, error }) => {
1111
const inputContainerStyle = {
1212
flex: '1 0 45%',
1313
marginBottom: '0.75rem',
@@ -25,12 +25,29 @@ const ParameterInput = ({ label, value, onChange }) => {
2525
fontFamily: fontFamily
2626
};
2727

28+
const hintStyle = {
29+
display: 'block',
30+
fontSize: '0.85rem',
31+
color: '#6B7280',
32+
marginTop: '0.25rem',
33+
fontFamily: fontFamily
34+
};
35+
36+
const errorStyle = {
37+
display: 'block',
38+
fontSize: '0.85rem',
39+
color: '#DC2626',
40+
marginTop: '0.25rem',
41+
fontFamily: fontFamily,
42+
fontWeight: '500'
43+
};
44+
2845
const inputStyle = {
2946
display: 'block',
3047
width: '100%',
3148
padding: '0.5rem 0.7rem',
3249
backgroundColor: 'white',
33-
border: '1px solid #D1D5DB',
50+
border: error ? '2px solid #DC2626' : '1px solid #D1D5DB',
3451
borderRadius: '0.35rem',
3552
boxSizing: 'border-box',
3653
color: '#000',
@@ -65,6 +82,7 @@ const ParameterInput = ({ label, value, onChange }) => {
6582
style={inputStyle}
6683
placeholder="Enter value"
6784
/>
85+
{error ? <span style={errorStyle}>{error}</span> : hint && <span style={hintStyle}>{hint}</span>}
6886
</div>
6987
);
7088
};
@@ -81,30 +99,69 @@ const ResourceEstimator = () => {
8199
const [estimatedQPU, setEstimatedQPU] = useState(null);
82100
const [history, setHistory] = useState([]);
83101
const [basket, setBasket] = useState([]);
102+
const [validationErrors, setValidationErrors] = useState({});
84103

85104
// Load history and basket from localStorage on component mount
86105
useEffect(() => {
87106
setHistory(JobHistoryManager.loadHistory());
88107
setBasket(JobHistoryManager.loadBasket());
89108
}, []);
90109

110+
// Re-validate qubits when device changes
111+
useEffect(() => {
112+
if (formData.qubits) {
113+
validateField('qubits', formData.qubits);
114+
}
115+
}, [selectedDevice]);
116+
117+
const validateField = (field, value) => {
118+
const numValue = parseInt(value, 10);
119+
const newErrors = { ...validationErrors };
120+
121+
if (field === 'qubits') {
122+
const deviceConfig = DEVICE_PARAMS[selectedDevice];
123+
if (value === '' || value === undefined || value === null) {
124+
newErrors.qubits = 'Qubits is required';
125+
} else if (numValue < 1) {
126+
newErrors.qubits = 'Must be at least 1 qubit';
127+
} else if (deviceConfig.max_qubits && numValue > deviceConfig.max_qubits) {
128+
newErrors.qubits = `${deviceConfig.name} supports max ${deviceConfig.max_qubits} qubits`;
129+
} else {
130+
delete newErrors.qubits;
131+
}
132+
} else {
133+
// Validate other fields for positive values
134+
if (value === '' || value === undefined || value === null) {
135+
newErrors[field] = 'This field is required';
136+
} else if (numValue < 1) {
137+
newErrors[field] = 'Must be a positive integer';
138+
} else {
139+
delete newErrors[field];
140+
}
141+
}
142+
143+
setValidationErrors(newErrors);
144+
};
145+
91146
const handleInputChange = (field, value) => {
92147
console.log(`Updating ${field} to ${value}`);
93148
setFormData(prevState => ({
94149
...prevState,
95150
[field]: value
96151
}));
152+
validateField(field, value);
97153
};
98154

99155
const calculateQPU = () => {
100-
// Validate all inputs are present before calculating
156+
// Validate all fields first
101157
const requiredFields = ['batches', 'depth', 'shots', 'qubits'];
102-
const missingFields = requiredFields.filter(field =>
103-
formData[field] === '' || formData[field] === undefined || formData[field] === null
104-
);
158+
requiredFields.forEach(field => validateField(field, formData[field]));
159+
160+
// Check if there are any validation errors
161+
const hasErrors = Object.keys(validationErrors).length > 0 ||
162+
requiredFields.some(field => formData[field] === '' || formData[field] === undefined || formData[field] === null);
105163

106-
if (missingFields.length > 0) {
107-
alert('Please fill in all parameter fields before calculating.');
164+
if (hasErrors) {
108165
return;
109166
}
110167

@@ -114,7 +171,7 @@ const ResourceEstimator = () => {
114171
depth: parseInt(formData.depth, 10),
115172
shots: parseInt(formData.shots, 10),
116173
qubits: parseInt(formData.qubits, 10)
117-
};
174+
}
118175

119176
// Calculate QPU seconds using our model
120177
const qpuSeconds = calculateQPUSeconds(selectedDevice, numericFormData);
@@ -273,8 +330,10 @@ const ResourceEstimator = () => {
273330
<ParameterInput
274331
label="Number of Qubits"
275332
value={formData.qubits}
276-
onChange={(value) => handleInputChange('qubits', value)}
277-
/>
333+
onChange={(value) => handleInputChange('qubits', value)}
334+
hint={`Max: ${DEVICE_PARAMS[selectedDevice].max_qubits} qubits`}
335+
error={validationErrors.qubits}
336+
/>
278337
</div>
279338

280339
<div style={{marginTop: '1rem', textAlign: 'center'}}>

src/utils/ResourceEstimatorModel.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
const DEVICE_PARAMS = {
99
helmi: {
1010
name: 'Helmi',
11+
max_qubits: 5,
1112
intercept: 2.361885,
1213
terms: [
1314
{type: 'interaction', variables: ['batches', 'kshots'], coefficient: 0.432804},
@@ -27,6 +28,7 @@ const DEVICE_PARAMS = {
2728
},
2829
'vtt-q50': {
2930
name: 'VTT Q50',
31+
max_qubits: 54,
3032
// Model: Log-transform (captures multiplicative structure)
3133
logTransform: true, // Model trained on log(y), must apply exp()
3234
epsilon: 0.001000,

test_js_python_consistency.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
// Test JavaScript model consistency with Python
2-
import { calculateQPUSeconds } from './src/utils/ResourceEstimatorModel.js';
2+
import { calculateQPUSeconds, DEVICE_PARAMS } from './src/utils/ResourceEstimatorModel.js';
3+
4+
// First, verify device configurations
5+
console.log('Verifying device configurations...');
6+
console.log('Helmi max_qubits:', DEVICE_PARAMS.helmi.max_qubits);
7+
console.log('VTT Q50 max_qubits:', DEVICE_PARAMS['vtt-q50'].max_qubits);
8+
9+
if (DEVICE_PARAMS.helmi.max_qubits !== 5) {
10+
console.error('❌ Helmi should have max_qubits = 5');
11+
process.exit(1);
12+
}
13+
14+
if (DEVICE_PARAMS['vtt-q50'].max_qubits !== 54) {
15+
console.error('❌ VTT Q50 should have max_qubits = 54');
16+
process.exit(1);
17+
}
18+
19+
console.log('✅ Device configurations are correct\n');
320

421
// Test cases from CSV (verify JavaScript matches Python, not necessarily actual values)
522
// The goal is to ensure JS and Python produce identical predictions

0 commit comments

Comments
 (0)