Skip to main content

qualia_client_core/
qpu_dispatcher.rs

1//! HTTP egress to all supported QPU providers — blind numeric payloads only.
2//!
3//! Each provider receives only anonymised QUBO matrices or VQE parameter vectors.
4//! Classified semantic data is blocked by the Sentinel before reaching this layer.
5
6use 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
10// ── Provider endpoints ────────────────────────────────────────────────────────
11
12const 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// Azure and Braket endpoints are workspace-scoped, constructed at call time.
18
19// ── Result type ───────────────────────────────────────────────────────────────
20
21#[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
32// ── Main dispatch entry-points ─────────────────────────────────────────────────
33
34pub 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    // Try D-Wave first (purpose-built annealer for QUBO)
47    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    // Try IonQ (trapped-ion gate-model — can handle small QUBO via QAOA)
79    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, &region, 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    // Prefer Quantinuum (best gate fidelity for VQE) → IonQ → IBM → Rigetti → Google
186    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, &region, 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
374// ── D-Wave ─────────────────────────────────────────────────────────────────────
375
376fn 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
399// ── IonQ ──────────────────────────────────────────────────────────────────────
400
401fn submit_ionq_qubo(token: &str, matrix: &QuboMatrix, shots: u32) -> Result<String, String> {
402    // Encode QUBO as a QAOA-style circuit for IonQ
403    let n = matrix.num_vars as usize;
404    let mut gates = Vec::new();
405    // Initial Hadamard layer
406    for i in 0..n {
407        gates.push(serde_json::json!({"gate": "h", "target": i}));
408    }
409    // QUBO penalty terms as Rz / ZZ rotations
410    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
445// ── IBM Quantum ───────────────────────────────────────────────────────────────
446
447fn 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
465// ── Rigetti QCS ───────────────────────────────────────────────────────────────
466
467fn submit_rigetti_vqe(token: &str, params: &[f64], shots: u32) -> Result<String, String> {
468    // Rigetti uses Quil programs; send a parameterised VQE template
469    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
488// ── Quantinuum ────────────────────────────────────────────────────────────────
489
490fn submit_quantinuum_vqe(token: &str, params: &[f64], shots: u32) -> Result<String, String> {
491    // Quantinuum accepts OpenQASM 2.0
492    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
515// ── Google Quantum AI ─────────────────────────────────────────────────────────
516
517fn submit_google_vqe(token: &str, params: &[f64], shots: u32) -> Result<String, String> {
518    // Google Quantum AI uses Cirq serialised circuits via REST
519    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    // Google uses OAuth2 Bearer
545    http_post_sync(
546        "https://quantum.googleapis.com/v1/projects/qualia/programs:run",
547        token,
548        "Bearer",
549        &body,
550    )
551}
552
553// ── Amazon Braket ─────────────────────────────────────────────────────────────
554
555fn 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
632// ── Azure Quantum ─────────────────────────────────────────────────────────────
633
634fn 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
713// ── AWS SigV4 helper ──────────────────────────────────────────────────────────
714
715fn 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    // Derive current UTC timestamp without external deps
736    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    // Canonical request
743    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    // Derive signing key
761    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
815/// Format Unix seconds as `(YYYYMMDDTHHMMSSZ, YYYYMMDD)` without chrono.
816fn format_iso8601(secs: u64) -> (String, String) {
817    // Days since epoch
818    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    // Gregorian calendar from day count (Fliegel-Van Flandern algorithm)
825    let jd = days as i64 + 2_440_588; // days since J2000 epoch offset
826    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
842// ── HTTP helper ───────────────────────────────────────────────────────────────
843
844fn 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}