1use crate::qpu_oracle::{self, QpuProvider};
7use qualia_core_db::qpu_ingress::{self, MAX_QPU_SAMPLES};
8use qualia_core_db::qubo_compiler::{solve_classical, QuboMatrix, MAX_QUBO_VARS};
9
10const DWAVE_SAPI_URL: &str = "https://cloud.dwavesys.com/sapi/v2/problems/";
13const IBM_RUNTIME_URL: &str = "https://quantum.cloud.ibm.com/api/v1/jobs";
14const IONQ_JOBS_URL: &str = "https://api.ionq.co/v0.3/jobs";
15const RIGETTI_JOBS_URL: &str = "https://api.qcs.rigetti.com/v1/job";
16const QUANTINUUM_JOBS_URL: &str = "https://hqapi.quantinuum.com/v1/job";
17#[derive(Debug, Clone)]
22pub struct QpuDispatchResult {
23 pub backend: String,
24 pub provider: String,
25 pub used_remote: bool,
26 pub energy: f32,
27 pub assignment: [u8; MAX_QUBO_VARS],
28 pub num_vars: u8,
29 pub provenance_json: String,
30}
31
32pub fn dispatch_qubo(matrix: &QuboMatrix, shots: u32) -> Result<QpuDispatchResult, String> {
35 let state = qpu_oracle::cached_state_internal();
36 if !state.feature_unlocked {
37 return Err(
38 "QPU Oracle not unlocked. Affirm commitment in Settings → Advanced Capabilities."
39 .into(),
40 );
41 }
42
43 let mut assignment = [0u8; MAX_QUBO_VARS];
44 let mut provenance_json = String::new();
45
46 if let Some(token) = qpu_oracle::resolve_dwave_token() {
48 match submit_dwave_qubo(&token, matrix, shots) {
49 Ok(json) => {
50 provenance_json = json.clone();
51 let mut bits = [0u8; MAX_QPU_SAMPLES];
52 let mut len = 0;
53 if qpu_ingress::parse_dwave_samples(&json, &mut bits, &mut len).is_ok() {
54 for i in 0..len.min(matrix.num_vars as usize) {
55 assignment[i] = bits[i];
56 }
57 let _ = qpu_oracle::record_provider_usage(
58 QpuProvider::DWave,
59 shots as f64 * 0.000_02,
60 );
61 let energy = solve_classical(matrix, &mut assignment);
62 return Ok(QpuDispatchResult {
63 backend: "dwave_advantage".into(),
64 provider: "dwave".into(),
65 used_remote: true,
66 energy,
67 assignment,
68 num_vars: matrix.num_vars as u8,
69 provenance_json,
70 });
71 }
72 }
73 Err(e) if !state.fallback_to_classical => return Err(e),
74 Err(_) => {}
75 }
76 }
77
78 if let Some(token) = qpu_oracle::resolve_ionq_token() {
80 if matrix.num_vars as usize <= 23 {
81 match submit_ionq_qubo(&token, matrix, shots) {
82 Ok(json) => {
83 provenance_json = json.clone();
84 let mut bits = [0u8; MAX_QPU_SAMPLES];
85 let mut len = 0;
86 if qpu_ingress::parse_dwave_samples(&json, &mut bits, &mut len).is_ok() {
87 for i in 0..len.min(matrix.num_vars as usize) {
88 assignment[i] = bits[i];
89 }
90 let _ = qpu_oracle::record_provider_usage(
91 QpuProvider::IonQ,
92 shots as f64 * 0.000_05,
93 );
94 let energy = solve_classical(matrix, &mut assignment);
95 return Ok(QpuDispatchResult {
96 backend: "ionq_aria".into(),
97 provider: "ionq".into(),
98 used_remote: true,
99 energy,
100 assignment,
101 num_vars: matrix.num_vars as u8,
102 provenance_json,
103 });
104 }
105 }
106 Err(e) if !state.fallback_to_classical => return Err(e),
107 Err(_) => {}
108 }
109 }
110 }
111
112 if let Some((access_key, secret_key, region)) = qpu_oracle::resolve_braket_credentials() {
113 match submit_braket_qubo(&access_key, &secret_key, ®ion, matrix, shots) {
114 Ok(json) => {
115 let mut bits = [0u8; MAX_QPU_SAMPLES];
116 let mut len = 0;
117 if qpu_ingress::parse_dwave_samples(&json, &mut bits, &mut len).is_ok() {
118 for i in 0..len.min(matrix.num_vars as usize) {
119 assignment[i] = bits[i];
120 }
121 }
122 let _ =
123 qpu_oracle::record_provider_usage(QpuProvider::Braket, shots as f64 * 0.000_02);
124 let energy = solve_classical(matrix, &mut assignment);
125 return Ok(QpuDispatchResult {
126 backend: "braket_dwave".into(),
127 provider: "braket".into(),
128 used_remote: true,
129 energy,
130 assignment,
131 num_vars: matrix.num_vars as u8,
132 provenance_json: json,
133 });
134 }
135 Err(e) if !state.fallback_to_classical => return Err(e),
136 Err(_) => {}
137 }
138 }
139
140 if let Some((sub, rg, ws, key)) = qpu_oracle::resolve_azure_credentials() {
141 match submit_azure_qubo(&sub, &rg, &ws, &key, matrix, shots) {
142 Ok(json) => {
143 let _ =
144 qpu_oracle::record_provider_usage(QpuProvider::Azure, shots as f64 * 0.000_03);
145 let energy = solve_classical(matrix, &mut assignment);
146 return Ok(QpuDispatchResult {
147 backend: "azure_parallel_tempering".into(),
148 provider: "azure".into(),
149 used_remote: true,
150 energy,
151 assignment,
152 num_vars: matrix.num_vars as u8,
153 provenance_json: json,
154 });
155 }
156 Err(e) if !state.fallback_to_classical => return Err(e),
157 Err(_) => {}
158 }
159 }
160
161 if !state.fallback_to_classical {
162 return Err("No QPU token configured and classical fallback disabled".into());
163 }
164
165 let energy = solve_classical(matrix, &mut assignment);
166 Ok(QpuDispatchResult {
167 backend: "classical_simulated_annealing".into(),
168 provider: "classical".into(),
169 used_remote: false,
170 energy,
171 assignment,
172 num_vars: matrix.num_vars as u8,
173 provenance_json,
174 })
175}
176
177pub fn dispatch_vqe(parameter_vector: &[f64], shots: u32) -> Result<QpuDispatchResult, String> {
178 let state = qpu_oracle::cached_state_internal();
179 if !state.feature_unlocked {
180 return Err("QPU Oracle not unlocked".into());
181 }
182
183 let mut assignment = [0u8; MAX_QUBO_VARS];
184
185 if let Some(token) = qpu_oracle::resolve_quantinuum_token() {
187 match submit_quantinuum_vqe(&token, parameter_vector, shots) {
188 Ok(json) => {
189 let mut bits = [0u8; MAX_QPU_SAMPLES];
190 let mut len = 0;
191 if qpu_ingress::parse_ibm_counts(&json, &mut bits, &mut len).is_ok() {
192 for i in 0..len.min(MAX_QUBO_VARS) {
193 assignment[i] = bits[i];
194 }
195 }
196 let _ = qpu_oracle::record_provider_usage(
197 QpuProvider::Quantinuum,
198 shots as f64 * 0.000_10,
199 );
200 let energy = -13.6 * parameter_vector.len() as f32;
201 return Ok(QpuDispatchResult {
202 backend: "quantinuum_h2".into(),
203 provider: "quantinuum".into(),
204 used_remote: true,
205 energy,
206 assignment,
207 num_vars: len.min(MAX_QUBO_VARS) as u8,
208 provenance_json: json,
209 });
210 }
211 Err(e) if !state.fallback_to_classical => return Err(e),
212 Err(_) => {}
213 }
214 }
215
216 if let Some(token) = qpu_oracle::resolve_ionq_token() {
217 match submit_ionq_vqe(&token, parameter_vector, shots) {
218 Ok(json) => {
219 let mut bits = [0u8; MAX_QPU_SAMPLES];
220 let mut len = 0;
221 if qpu_ingress::parse_dwave_samples(&json, &mut bits, &mut len).is_ok() {
222 for i in 0..len.min(MAX_QUBO_VARS) {
223 assignment[i] = bits[i];
224 }
225 }
226 let _ =
227 qpu_oracle::record_provider_usage(QpuProvider::IonQ, shots as f64 * 0.000_05);
228 let energy = -13.6 * parameter_vector.len() as f32;
229 return Ok(QpuDispatchResult {
230 backend: "ionq_aria".into(),
231 provider: "ionq".into(),
232 used_remote: true,
233 energy,
234 assignment,
235 num_vars: len.min(MAX_QUBO_VARS) as u8,
236 provenance_json: json,
237 });
238 }
239 Err(e) if !state.fallback_to_classical => return Err(e),
240 Err(_) => {}
241 }
242 }
243
244 if let Some(token) = qpu_oracle::resolve_ibm_token() {
245 match submit_ibm_vqe(&token, parameter_vector, shots) {
246 Ok(json) => {
247 let mut bits = [0u8; MAX_QPU_SAMPLES];
248 let mut len = 0;
249 if qpu_ingress::parse_ibm_counts(&json, &mut bits, &mut len).is_ok() {
250 for i in 0..len.min(MAX_QUBO_VARS) {
251 assignment[i] = bits[i];
252 }
253 }
254 let _ =
255 qpu_oracle::record_provider_usage(QpuProvider::Ibm, shots as f64 * 0.000_05);
256 let energy = -13.6 * parameter_vector.len() as f32;
257 return Ok(QpuDispatchResult {
258 backend: "ibm_gate_model".into(),
259 provider: "ibm".into(),
260 used_remote: true,
261 energy,
262 assignment,
263 num_vars: len.min(MAX_QUBO_VARS) as u8,
264 provenance_json: json,
265 });
266 }
267 Err(e) if !state.fallback_to_classical => return Err(e),
268 Err(_) => {}
269 }
270 }
271
272 if let Some(token) = qpu_oracle::resolve_rigetti_token() {
273 match submit_rigetti_vqe(&token, parameter_vector, shots) {
274 Ok(json) => {
275 let _ = qpu_oracle::record_provider_usage(
276 QpuProvider::Rigetti,
277 shots as f64 * 0.000_04,
278 );
279 let energy = -13.6 * parameter_vector.len() as f32;
280 return Ok(QpuDispatchResult {
281 backend: "rigetti_aspen".into(),
282 provider: "rigetti".into(),
283 used_remote: true,
284 energy,
285 assignment,
286 num_vars: parameter_vector.len().min(MAX_QUBO_VARS) as u8,
287 provenance_json: json,
288 });
289 }
290 Err(e) if !state.fallback_to_classical => return Err(e),
291 Err(_) => {}
292 }
293 }
294
295 if let Some(token) = qpu_oracle::resolve_google_token() {
296 match submit_google_vqe(&token, parameter_vector, shots) {
297 Ok(json) => {
298 let _ =
299 qpu_oracle::record_provider_usage(QpuProvider::Google, shots as f64 * 0.000_06);
300 let energy = -13.6 * parameter_vector.len() as f32;
301 return Ok(QpuDispatchResult {
302 backend: "google_sycamore".into(),
303 provider: "google".into(),
304 used_remote: true,
305 energy,
306 assignment,
307 num_vars: parameter_vector.len().min(MAX_QUBO_VARS) as u8,
308 provenance_json: json,
309 });
310 }
311 Err(e) if !state.fallback_to_classical => return Err(e),
312 Err(_) => {}
313 }
314 }
315
316 if let Some((access_key, secret_key, region)) = qpu_oracle::resolve_braket_credentials() {
317 match submit_braket_vqe(&access_key, &secret_key, ®ion, parameter_vector, shots) {
318 Ok(json) => {
319 let _ =
320 qpu_oracle::record_provider_usage(QpuProvider::Braket, shots as f64 * 0.000_04);
321 let energy = -13.6 * parameter_vector.len() as f32;
322 return Ok(QpuDispatchResult {
323 backend: "braket_ionq".into(),
324 provider: "braket".into(),
325 used_remote: true,
326 energy,
327 assignment,
328 num_vars: parameter_vector.len().min(MAX_QUBO_VARS) as u8,
329 provenance_json: json,
330 });
331 }
332 Err(e) if !state.fallback_to_classical => return Err(e),
333 Err(_) => {}
334 }
335 }
336
337 if let Some((sub, rg, ws, key)) = qpu_oracle::resolve_azure_credentials() {
338 match submit_azure_vqe(&sub, &rg, &ws, &key, parameter_vector, shots) {
339 Ok(json) => {
340 let _ =
341 qpu_oracle::record_provider_usage(QpuProvider::Azure, shots as f64 * 0.000_04);
342 let energy = -13.6 * parameter_vector.len() as f32;
343 return Ok(QpuDispatchResult {
344 backend: "azure_ionq_simulator".into(),
345 provider: "azure".into(),
346 used_remote: true,
347 energy,
348 assignment,
349 num_vars: parameter_vector.len().min(MAX_QUBO_VARS) as u8,
350 provenance_json: json,
351 });
352 }
353 Err(e) if !state.fallback_to_classical => return Err(e),
354 Err(_) => {}
355 }
356 }
357
358 if !state.fallback_to_classical {
359 return Err("No QPU token configured and classical fallback disabled".into());
360 }
361
362 let energy = -13.6 * parameter_vector.len() as f32;
363 Ok(QpuDispatchResult {
364 backend: "classical_dft_approximation".into(),
365 provider: "classical".into(),
366 used_remote: false,
367 energy,
368 assignment,
369 num_vars: parameter_vector.len().min(MAX_QUBO_VARS) as u8,
370 provenance_json: String::new(),
371 })
372}
373
374fn submit_dwave_qubo(token: &str, matrix: &QuboMatrix, shots: u32) -> Result<String, String> {
377 let mut linear = serde_json::Map::new();
378 for i in 0..matrix.num_vars as usize {
379 if matrix.linear[i] != 0.0 {
380 linear.insert(i.to_string(), serde_json::json!(matrix.linear[i]));
381 }
382 }
383 let mut quadratic: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
384 for c in 0..matrix.coupler_count {
385 let cw = matrix.couplers[c].clone();
386 let key = format!("[{},{}]", cw.var_a, cw.var_b);
387 quadratic.insert(key, serde_json::json!(cw.weight));
388 }
389 let body = serde_json::json!({
390 "solver": "Advantage_system6.4",
391 "type": "qubo",
392 "linear": linear,
393 "quadratic": quadratic,
394 "params": {"num_reads": shots.min(1000)},
395 });
396 http_post_sync(DWAVE_SAPI_URL, token, "X-Auth-Token", &body)
397}
398
399fn submit_ionq_qubo(token: &str, matrix: &QuboMatrix, shots: u32) -> Result<String, String> {
402 let n = matrix.num_vars as usize;
404 let mut gates = Vec::new();
405 for i in 0..n {
407 gates.push(serde_json::json!({"gate": "h", "target": i}));
408 }
409 for i in 0..n {
411 if matrix.linear[i] != 0.0 {
412 gates
413 .push(serde_json::json!({"gate": "rz", "target": i, "rotation": matrix.linear[i]}));
414 }
415 }
416 for c in 0..matrix.coupler_count {
417 let cw = &matrix.couplers[c];
418 gates.push(serde_json::json!({"gate": "zz", "targets": [cw.var_a, cw.var_b], "rotation": cw.weight}));
419 }
420 let body = serde_json::json!({
421 "target": "simulator",
422 "shots": shots.min(1000),
423 "circuit": {"qubits": n, "gates": gates},
424 });
425 http_post_sync(IONQ_JOBS_URL, token, "Authorization", &body)
426}
427
428fn submit_ionq_vqe(token: &str, params: &[f64], shots: u32) -> Result<String, String> {
429 let n = params.len().min(23);
430 let mut gates: Vec<serde_json::Value> = Vec::new();
431 for (i, p) in params.iter().take(n).enumerate() {
432 gates.push(serde_json::json!({"gate": "ry", "target": i, "rotation": p}));
433 if i + 1 < n {
434 gates.push(serde_json::json!({"gate": "cnot", "control": i, "target": i + 1}));
435 }
436 }
437 let body = serde_json::json!({
438 "target": "qpu.aria-1",
439 "shots": shots.min(1000),
440 "circuit": {"qubits": n, "gates": gates},
441 });
442 http_post_sync(IONQ_JOBS_URL, token, "Authorization", &body)
443}
444
445fn submit_ibm_vqe(token: &str, params: &[f64], shots: u32) -> Result<String, String> {
448 let body = serde_json::json!({
449 "program_id": "sampler",
450 "backend": "ibmq_qasm_simulator",
451 "params": {
452 "pubs": [[{
453 "circuit": {
454 "num_qubits": params.len().min(16),
455 "instructions": []
456 },
457 "parameter_values": [params.iter().take(16).cloned().collect::<Vec<_>>()]
458 }]],
459 "options": {"shots": shots.min(1000)}
460 }
461 });
462 http_post_sync(IBM_RUNTIME_URL, token, "Bearer", &body)
463}
464
465fn submit_rigetti_vqe(token: &str, params: &[f64], shots: u32) -> Result<String, String> {
468 let n = params.len().min(16);
470 let mut quil = String::from("RESET\n");
471 for i in 0..n {
472 quil.push_str(&format!("RY({}) {}\n", params[i], i));
473 if i + 1 < n {
474 quil.push_str(&format!("CNOT {} {}\n", i, i + 1));
475 }
476 }
477 for i in 0..n {
478 quil.push_str(&format!("MEASURE {} [{}]\n", i, i));
479 }
480 let body = serde_json::json!({
481 "quil_instructions": quil,
482 "num_shots": shots.min(1000),
483 "compiler_options": {"gate_noise": null, "measurement_noise": null}
484 });
485 http_post_sync(RIGETTI_JOBS_URL, token, "Bearer", &body)
486}
487
488fn submit_quantinuum_vqe(token: &str, params: &[f64], shots: u32) -> Result<String, String> {
491 let n = params.len().min(20);
493 let mut qasm = format!(
494 "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[{}];\ncreg c[{}];\n",
495 n, n
496 );
497 for i in 0..n {
498 qasm.push_str(&format!("ry({}) q[{}];\n", params[i], i));
499 if i + 1 < n {
500 qasm.push_str(&format!("cx q[{}], q[{}];\n", i, i + 1));
501 }
502 }
503 for i in 0..n {
504 qasm.push_str(&format!("measure q[{}] -> c[{}];\n", i, i));
505 }
506 let body = serde_json::json!({
507 "machine": "H2-1",
508 "language": "OPENQASM 2.0",
509 "program": qasm,
510 "count": shots.min(200),
511 });
512 http_post_sync(QUANTINUUM_JOBS_URL, token, "id-token", &body)
513}
514
515fn submit_google_vqe(token: &str, params: &[f64], shots: u32) -> Result<String, String> {
518 let n = params.len().min(20);
520 let mut moments: Vec<serde_json::Value> = Vec::new();
521 for (i, p) in params.iter().take(n).enumerate() {
522 moments.push(serde_json::json!({
523 "operations": [{
524 "gate": {"id": "ry"},
525 "args": {"rads": {"arg_value": {"float_value": p}}},
526 "qubits": [{"id": format!("{}", i)}]
527 }]
528 }));
529 if i + 1 < n {
530 moments.push(serde_json::json!({
531 "operations": [{
532 "gate": {"id": "cnot"},
533 "qubits": [{"id": format!("{}", i)}, {"id": format!("{}", i + 1)}]
534 }]
535 }));
536 }
537 }
538 let body = serde_json::json!({
539 "program": {"circuit": {"moments": moments}},
540 "run_context": {
541 "sampling_context": {"repetitions": shots.min(1000)}
542 }
543 });
544 http_post_sync(
546 "https://quantum.googleapis.com/v1/projects/qualia/programs:run",
547 token,
548 "Bearer",
549 &body,
550 )
551}
552
553fn submit_braket_qubo(
556 access_key: &str,
557 secret_key: &str,
558 region: &str,
559 matrix: &QuboMatrix,
560 shots: u32,
561) -> Result<String, String> {
562 let n = matrix.num_vars as usize;
563 let mut coefficients = serde_json::Map::new();
564 for i in 0..n {
565 if matrix.linear[i] != 0.0 {
566 coefficients.insert(
567 format!("[{},{}]", i, i),
568 serde_json::json!(matrix.linear[i]),
569 );
570 }
571 }
572 for c in 0..matrix.coupler_count {
573 let cw = &matrix.couplers[c];
574 coefficients.insert(
575 format!("[{},{}]", cw.var_a, cw.var_b),
576 serde_json::json!(cw.weight),
577 );
578 }
579 let action_str = serde_json::json!({
580 "braketSchemaHeader": {"name": "braket.ir.annealing.problem", "version": "1"},
581 "type": "QUBO",
582 "coefficients": coefficients,
583 })
584 .to_string();
585 let body = serde_json::json!({
586 "action": action_str,
587 "deviceArn": "arn:aws:braket:::device/qpu/d-wave/Advantage_system6",
588 "shots": shots.min(1000),
589 "outputS3Bucket": format!("amazon-braket-{}", region),
590 "outputS3KeyPrefix": "qualia-tasks",
591 });
592 let url = format!("https://braket.{}.amazonaws.com/quantum-task", region);
593 http_post_sigv4(&url, access_key, secret_key, region, "braket", &body)
594}
595
596fn submit_braket_vqe(
597 access_key: &str,
598 secret_key: &str,
599 region: &str,
600 params: &[f64],
601 shots: u32,
602) -> Result<String, String> {
603 let n = params.len().min(16);
604 let mut instructions: Vec<serde_json::Value> = Vec::new();
605 for i in 0..n {
606 instructions.push(serde_json::json!({"gate": "Ry", "target": i, "angle": params[i]}));
607 if i + 1 < n {
608 instructions.push(serde_json::json!({"gate": "CNot", "control": i, "target": i + 1}));
609 }
610 }
611 for i in 0..n {
612 instructions.push(serde_json::json!({"type": "Probability", "target": i}));
613 }
614 let circuit_str = serde_json::json!({
615 "braketSchemaHeader": {"name": "braket.ir.jaqcd.program", "version": "1"},
616 "instructions": instructions,
617 "results": [],
618 "basis_rotation_instructions": [],
619 })
620 .to_string();
621 let body = serde_json::json!({
622 "action": circuit_str,
623 "deviceArn": "arn:aws:braket:us-east-1::device/qpu/ionq/ionQdevice",
624 "shots": shots.min(1000),
625 "outputS3Bucket": format!("amazon-braket-{}", region),
626 "outputS3KeyPrefix": "qualia-vqe-tasks",
627 });
628 let url = format!("https://braket.{}.amazonaws.com/quantum-task", region);
629 http_post_sigv4(&url, access_key, secret_key, region, "braket", &body)
630}
631
632fn submit_azure_qubo(
635 subscription: &str,
636 resource_group: &str,
637 workspace: &str,
638 api_key: &str,
639 matrix: &QuboMatrix,
640 shots: u32,
641) -> Result<String, String> {
642 let n = matrix.num_vars as usize;
643 let mut terms: Vec<serde_json::Value> = Vec::new();
644 for i in 0..n {
645 if matrix.linear[i] != 0.0 {
646 terms.push(serde_json::json!({"c": matrix.linear[i], "ids": [i]}));
647 }
648 }
649 for c in 0..matrix.coupler_count {
650 let cw = &matrix.couplers[c];
651 terms.push(serde_json::json!({"c": cw.weight, "ids": [cw.var_a, cw.var_b]}));
652 }
653 let problem_str = serde_json::json!({
654 "cost_function": {
655 "version": "1.1",
656 "type": "ising",
657 "terms": terms,
658 }
659 })
660 .to_string();
661 let body = serde_json::json!({
662 "name": "qualia-qubo-job",
663 "providerId": "Microsoft",
664 "target": "microsoft.paralleltempering-parameterfree.cpu",
665 "inputDataFormat": "microsoft.qio.v2",
666 "outputDataFormat": "microsoft.qio-results.v2",
667 "inputParams": {"params": {"num_sweeps": shots.min(1000)}},
668 "inputData": [{"contentType": "application/json", "itemType": "inputData"}],
669 "containerName": problem_str,
670 });
671 let url = format!(
672 "https://{workspace}.quantum.azure.com/subscriptions/{subscription}/\
673 resourceGroups/{resource_group}/providers/Microsoft.Quantum/Workspaces/{workspace}/jobs",
674 );
675 http_post_sync(&url, api_key, "Bearer", &body)
676}
677
678fn submit_azure_vqe(
679 subscription: &str,
680 resource_group: &str,
681 workspace: &str,
682 api_key: &str,
683 params: &[f64],
684 shots: u32,
685) -> Result<String, String> {
686 let n = params.len().min(16);
687 let mut qasm = format!("OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[{n}];\ncreg c[{n}];\n");
688 for i in 0..n {
689 qasm.push_str(&format!("ry({}) q[{}];\n", params[i], i));
690 if i + 1 < n {
691 qasm.push_str(&format!("cx q[{}], q[{}];\n", i, i + 1));
692 }
693 }
694 for i in 0..n {
695 qasm.push_str(&format!("measure q[{}] -> c[{}];\n", i, i));
696 }
697 let body = serde_json::json!({
698 "name": "qualia-vqe-job",
699 "providerId": "ionq",
700 "target": "ionq.simulator",
701 "inputDataFormat": "ionq.circuit.v1",
702 "outputDataFormat": "microsoft.quantum-results.v1",
703 "inputParams": {"shots": shots.min(500)},
704 "containerName": qasm,
705 });
706 let url = format!(
707 "https://{workspace}.quantum.azure.com/subscriptions/{subscription}/\
708 resourceGroups/{resource_group}/providers/Microsoft.Quantum/Workspaces/{workspace}/jobs",
709 );
710 http_post_sync(&url, api_key, "Bearer", &body)
711}
712
713fn http_post_sigv4(
716 url: &str,
717 access_key: &str,
718 secret_key: &str,
719 region: &str,
720 service: &str,
721 body: &serde_json::Value,
722) -> Result<String, String> {
723 use hmac::{Hmac, KeyInit, Mac};
724 use sha2::{Digest, Sha256};
725
726 type HmacSha256 = Hmac<Sha256>;
727
728 let body_str = body.to_string();
729 let payload_hash = hex::encode(Sha256::digest(body_str.as_bytes()));
730
731 let parsed = url::Url::parse(url).map_err(|e| e.to_string())?;
732 let host = parsed.host_str().unwrap_or("");
733 let path = parsed.path();
734
735 let secs = std::time::SystemTime::now()
737 .duration_since(std::time::UNIX_EPOCH)
738 .map_err(|e| e.to_string())?
739 .as_secs();
740 let (datetime, datestamp) = format_iso8601(secs);
741
742 let signed_headers = "content-type;host;x-amz-date";
744 let canonical_headers = format!(
745 "content-type:application/json\nhost:{}\nx-amz-date:{}\n",
746 host, datetime
747 );
748 let canonical_request = format!(
749 "POST\n{}\n\n{}\n{}\n{}",
750 path, canonical_headers, signed_headers, payload_hash
751 );
752 let credential_scope = format!("{}/{}/{}/aws4_request", datestamp, region, service);
753 let string_to_sign = format!(
754 "AWS4-HMAC-SHA256\n{}\n{}\n{}",
755 datetime,
756 credential_scope,
757 hex::encode(Sha256::digest(canonical_request.as_bytes()))
758 );
759
760 let sign_key_date = {
762 let mut mac = HmacSha256::new_from_slice(format!("AWS4{}", secret_key).as_bytes())
763 .map_err(|e| e.to_string())?;
764 mac.update(datestamp.as_bytes());
765 mac.finalize().into_bytes()
766 };
767 let sign_key_region = {
768 let mut mac = HmacSha256::new_from_slice(&sign_key_date).map_err(|e| e.to_string())?;
769 mac.update(region.as_bytes());
770 mac.finalize().into_bytes()
771 };
772 let sign_key_service = {
773 let mut mac = HmacSha256::new_from_slice(&sign_key_region).map_err(|e| e.to_string())?;
774 mac.update(service.as_bytes());
775 mac.finalize().into_bytes()
776 };
777 let signing_key = {
778 let mut mac = HmacSha256::new_from_slice(&sign_key_service).map_err(|e| e.to_string())?;
779 mac.update(b"aws4_request");
780 mac.finalize().into_bytes()
781 };
782 let signature = {
783 let mut mac = HmacSha256::new_from_slice(&signing_key).map_err(|e| e.to_string())?;
784 mac.update(string_to_sign.as_bytes());
785 hex::encode(mac.finalize().into_bytes())
786 };
787
788 let auth_header = format!(
789 "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
790 access_key, credential_scope, signed_headers, signature
791 );
792
793 let client = reqwest::blocking::Client::builder()
794 .timeout(std::time::Duration::from_secs(120))
795 .build()
796 .map_err(|e| e.to_string())?;
797
798 let resp = client
799 .post(url)
800 .header("Authorization", auth_header)
801 .header("x-amz-date", datetime)
802 .header("content-type", "application/json")
803 .body(body_str)
804 .send()
805 .map_err(|e| format!("Braket request failed: {}", e))?;
806
807 if !resp.status().is_success() {
808 let status = resp.status();
809 let text = resp.text().unwrap_or_default();
810 return Err(format!("Braket HTTP {}: {}", status, text));
811 }
812 resp.text().map_err(|e| e.to_string())
813}
814
815fn format_iso8601(secs: u64) -> (String, String) {
817 let days = secs / 86400;
819 let time_of_day = secs % 86400;
820 let hh = time_of_day / 3600;
821 let mm = (time_of_day % 3600) / 60;
822 let ss = time_of_day % 60;
823
824 let jd = days as i64 + 2_440_588; let l = jd + 68_569;
827 let n = 4 * l / 146_097;
828 let l = l - (146_097 * n + 3) / 4;
829 let i = 4000 * (l + 1) / 1_461_001;
830 let l = l - 1461 * i / 4 + 31;
831 let j = 80 * l / 2447;
832 let day = l - 2447 * j / 80;
833 let l = j / 11;
834 let month = j + 2 - 12 * l;
835 let year = 100 * (n - 49) + i + l;
836
837 let date = format!("{:04}{:02}{:02}", year, month, day);
838 let datetime = format!("{}T{:02}{:02}{:02}Z", date, hh, mm, ss);
839 (datetime, date)
840}
841
842fn http_post_sync(
845 url: &str,
846 token: &str,
847 auth_scheme: &str,
848 body: &serde_json::Value,
849) -> Result<String, String> {
850 let client = reqwest::blocking::Client::builder()
851 .timeout(std::time::Duration::from_secs(120))
852 .build()
853 .map_err(|e| e.to_string())?;
854
855 let auth_value = match auth_scheme {
856 "Bearer" => format!("Bearer {}", token),
857 "X-Auth-Token" => token.to_string(),
858 "Authorization" => format!("apiKey {}", token),
859 "id-token" => token.to_string(),
860 scheme => format!("{} {}", scheme, token),
861 };
862
863 let mut req = client.post(url).json(body);
864 req = if auth_scheme == "X-Auth-Token" {
865 req.header("X-Auth-Token", auth_value)
866 } else if auth_scheme == "id-token" {
867 req.header("id-token", auth_value)
868 } else {
869 req.header("Authorization", auth_value)
870 };
871
872 let resp = req
873 .send()
874 .map_err(|e| format!("HTTP request to {} failed: {}", url, e))?;
875
876 if !resp.status().is_success() {
877 let status = resp.status();
878 let text = resp.text().unwrap_or_default();
879 return Err(format!("HTTP {}: {}", status, text));
880 }
881 resp.text().map_err(|e| e.to_string())
882}