Skip to main content

qualia_cli/
qpu.rs

1//! QPU provider configuration management.
2//!
3//! Stores per-provider API credentials in `$QUALIA_DATA_DIR/qpu_config.json`.
4//! Enabled at runtime via `--enable-qpu` on the top-level CLI.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10const CONFIG_FILE: &str = "qpu_config.json";
11
12// ── Provider metadata ─────────────────────────────────────────────────────────
13
14struct ProviderInfo {
15    id: &'static str,
16    name: &'static str,
17    problem_types: &'static str,
18    required: &'static [&'static str],
19    optional: &'static [&'static str],
20    docs: &'static str,
21}
22
23const PROVIDERS: &[ProviderInfo] = &[
24    ProviderInfo {
25        id: "ibm",
26        name: "IBM Quantum",
27        problem_types: "gate-model, vqe, qaoa",
28        required: &["api_key"],
29        optional: &["hub", "group", "project", "instance", "endpoint"],
30        docs: "https://quantum.ibm.com — get token from Account Settings",
31    },
32    ProviderInfo {
33        id: "dwave",
34        name: "D-Wave Leap",
35        problem_types: "annealing (QUBO)",
36        required: &["api_key"],
37        optional: &["endpoint", "solver"],
38        docs: "https://cloud.dwavesys.com/leap — get token from Dashboard > API Token",
39    },
40    ProviderInfo {
41        id: "ionq",
42        name: "IonQ",
43        problem_types: "gate-model",
44        required: &["api_key"],
45        optional: &["backend", "endpoint"],
46        docs: "https://cloud.ionq.com — get key from API Keys section",
47    },
48    ProviderInfo {
49        id: "rigetti",
50        name: "Rigetti QCS",
51        problem_types: "gate-model, vqe, qaoa",
52        required: &["api_key", "user_id"],
53        optional: &["qpu_id", "endpoint"],
54        docs: "https://qcs.rigetti.com — get credentials from QCS Settings",
55    },
56    ProviderInfo {
57        id: "azure",
58        name: "Azure Quantum",
59        problem_types: "gate-model, annealing, vqe, qaoa",
60        required: &["subscription_id", "resource_group", "workspace", "location"],
61        optional: &["api_key", "endpoint"],
62        docs: "https://portal.azure.com — create Azure Quantum workspace",
63    },
64    ProviderInfo {
65        id: "braket",
66        name: "AWS Braket",
67        problem_types: "gate-model, annealing",
68        required: &["access_key_id", "secret_access_key", "region"],
69        optional: &["s3_bucket", "endpoint"],
70        docs: "https://aws.amazon.com/braket — use IAM credentials with AmazonBraketFullAccess",
71    },
72    ProviderInfo {
73        id: "google",
74        name: "Google Quantum AI",
75        problem_types: "gate-model",
76        required: &["project_id", "processor_id"],
77        optional: &["service_account_key_path", "endpoint"],
78        docs:
79            "https://quantumai.google — requires Cloud project with Quantum Computing Service API",
80    },
81    ProviderInfo {
82        id: "quantinuum",
83        name: "Quantinuum",
84        problem_types: "gate-model",
85        required: &["api_key"],
86        optional: &["machine", "endpoint"],
87        docs: "https://um.qapi.quantinuum.com — get credentials from Quantinuum account portal",
88    },
89];
90
91fn find_provider(id: &str) -> Option<&'static ProviderInfo> {
92    PROVIDERS.iter().find(|p| p.id == id)
93}
94
95// ── Config data model ─────────────────────────────────────────────────────────
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct ProviderConfig {
99    // universal
100    pub api_key: Option<String>,
101    pub endpoint: Option<String>,
102    // IBM
103    pub hub: Option<String>,
104    pub group: Option<String>,
105    pub project: Option<String>,
106    pub instance: Option<String>,
107    // Azure
108    pub subscription_id: Option<String>,
109    pub resource_group: Option<String>,
110    pub workspace: Option<String>,
111    pub location: Option<String>,
112    // Braket
113    pub access_key_id: Option<String>,
114    pub secret_access_key: Option<String>,
115    pub region: Option<String>,
116    pub s3_bucket: Option<String>,
117    // Google
118    pub project_id: Option<String>,
119    pub processor_id: Option<String>,
120    pub service_account_key_path: Option<String>,
121    // Rigetti
122    pub user_id: Option<String>,
123    pub qpu_id: Option<String>,
124    // IonQ
125    pub backend: Option<String>,
126    // Quantinuum
127    pub machine: Option<String>,
128}
129
130#[derive(Debug, Default, Serialize, Deserialize)]
131pub struct QpuConfigStore {
132    pub providers: HashMap<String, ProviderConfig>,
133}
134
135// ── Config file I/O ───────────────────────────────────────────────────────────
136
137fn config_path(data_dir: &str) -> std::path::PathBuf {
138    Path::new(data_dir).join(CONFIG_FILE)
139}
140
141pub fn load_config(data_dir: &str) -> QpuConfigStore {
142    let path = config_path(data_dir);
143    if !path.exists() {
144        return QpuConfigStore::default();
145    }
146    match std::fs::read_to_string(&path) {
147        Ok(s) => serde_json::from_str(&s).unwrap_or_default(),
148        Err(_) => QpuConfigStore::default(),
149    }
150}
151
152fn save_config(data_dir: &str, store: &QpuConfigStore) {
153    let path = config_path(data_dir);
154    if let Some(parent) = path.parent() {
155        let _ = std::fs::create_dir_all(parent);
156    }
157    match serde_json::to_string_pretty(store) {
158        Ok(s) => {
159            if let Err(e) = std::fs::write(&path, &s) {
160                eprintln!("QPU config write error: {e}");
161            } else {
162                println!("Config saved to: {}", path.display());
163                println!(
164                    "Note: API keys are stored in plaintext — restrict file permissions if needed."
165                );
166            }
167        }
168        Err(e) => eprintln!("QPU config serialize error: {e}"),
169    }
170}
171
172// ── Display helpers ───────────────────────────────────────────────────────────
173
174fn mask(s: &str) -> String {
175    if s.len() <= 8 {
176        return "*".repeat(s.len());
177    }
178    format!("{}...{}", &s[..4], &s[s.len() - 4..])
179}
180
181fn display_opt(v: &Option<String>, sensitive: bool) -> String {
182    match v {
183        None => "(not set)".into(),
184        Some(s) if s.is_empty() => "(empty)".into(),
185        Some(s) if sensitive => mask(s),
186        Some(s) => s.clone(),
187    }
188}
189
190// ── Command handlers ──────────────────────────────────────────────────────────
191
192pub fn run_list_providers() {
193    println!("================================================================");
194    println!(
195        "  QualiaDB QPU — Supported Providers ({} total)",
196        PROVIDERS.len()
197    );
198    println!("================================================================");
199    for p in PROVIDERS {
200        println!("\n  [{id}]  {name}", id = p.id, name = p.name);
201        println!("  Problem types : {}", p.problem_types);
202        println!("  Required      : {}", p.required.join(", "));
203        if !p.optional.is_empty() {
204            println!("  Optional      : {}", p.optional.join(", "));
205        }
206        println!("  Docs          : {}", p.docs);
207    }
208    println!("\n================================================================");
209    println!("Configure with:");
210    println!("  qualia-cli --enable-qpu qpu configure <provider-id> --api-key <key> [...]");
211    println!("================================================================");
212}
213
214#[allow(clippy::too_many_arguments)]
215pub fn run_configure(
216    data_dir: &str,
217    provider: &str,
218    api_key: Option<&str>,
219    endpoint: Option<&str>,
220    hub: Option<&str>,
221    group: Option<&str>,
222    project: Option<&str>,
223    instance: Option<&str>,
224    subscription_id: Option<&str>,
225    resource_group: Option<&str>,
226    workspace: Option<&str>,
227    location: Option<&str>,
228    access_key_id: Option<&str>,
229    secret_access_key: Option<&str>,
230    region: Option<&str>,
231    s3_bucket: Option<&str>,
232    project_id: Option<&str>,
233    processor_id: Option<&str>,
234    service_account_key_path: Option<&str>,
235    user_id: Option<&str>,
236    qpu_id: Option<&str>,
237    backend: Option<&str>,
238    machine: Option<&str>,
239) {
240    let Some(info) = find_provider(provider) else {
241        eprintln!(
242            "Unknown provider '{}'. Run `qpu list-providers` to see valid IDs.",
243            provider
244        );
245        return;
246    };
247
248    let mut store = load_config(data_dir);
249    let cfg = store.providers.entry(provider.to_string()).or_default();
250
251    // Apply any supplied field; leave existing values untouched when None
252    macro_rules! apply {
253        ($field:ident, $val:expr) => {
254            if let Some(v) = $val {
255                cfg.$field = Some(v.to_string());
256            }
257        };
258    }
259
260    apply!(api_key, api_key);
261    apply!(endpoint, endpoint);
262    apply!(hub, hub);
263    apply!(group, group);
264    apply!(project, project);
265    apply!(instance, instance);
266    apply!(subscription_id, subscription_id);
267    apply!(resource_group, resource_group);
268    apply!(workspace, workspace);
269    apply!(location, location);
270    apply!(access_key_id, access_key_id);
271    apply!(secret_access_key, secret_access_key);
272    apply!(region, region);
273    apply!(s3_bucket, s3_bucket);
274    apply!(project_id, project_id);
275    apply!(processor_id, processor_id);
276    apply!(service_account_key_path, service_account_key_path);
277    apply!(user_id, user_id);
278    apply!(qpu_id, qpu_id);
279    apply!(backend, backend);
280    apply!(machine, machine);
281
282    // Validate required fields are now set
283    let missing: Vec<&str> = info
284        .required
285        .iter()
286        .copied()
287        .filter(|&field| match field {
288            "api_key" => cfg.api_key.is_none(),
289            "subscription_id" => cfg.subscription_id.is_none(),
290            "resource_group" => cfg.resource_group.is_none(),
291            "workspace" => cfg.workspace.is_none(),
292            "location" => cfg.location.is_none(),
293            "access_key_id" => cfg.access_key_id.is_none(),
294            "secret_access_key" => cfg.secret_access_key.is_none(),
295            "region" => cfg.region.is_none(),
296            "project_id" => cfg.project_id.is_none(),
297            "processor_id" => cfg.processor_id.is_none(),
298            "user_id" => cfg.user_id.is_none(),
299            _ => false,
300        })
301        .collect();
302
303    println!("================================================================");
304    println!("  QPU Configure — {}", info.name);
305    println!("================================================================");
306
307    save_config(data_dir, &store);
308
309    if !missing.is_empty() {
310        println!("\nWarning: the following required fields are still not set:");
311        for f in &missing {
312            println!("  --{}", f.replace('_', "-"));
313        }
314        println!(
315            "Run `qpu test-connection {}` after supplying all required fields.",
316            provider
317        );
318    } else {
319        println!("All required fields set for {}.", info.name);
320        println!(
321            "Run `qualia-cli --enable-qpu qpu test-connection {}` to validate.",
322            provider
323        );
324    }
325}
326
327pub fn run_show(data_dir: &str, provider: Option<&str>) {
328    let store = load_config(data_dir);
329
330    let entries: Vec<(&str, &ProviderConfig)> = match provider {
331        Some(id) => {
332            if let Some(cfg) = store.providers.get(id) {
333                vec![(id, cfg)]
334            } else {
335                println!("Provider '{}' has no stored configuration.", id);
336                return;
337            }
338        }
339        None => store
340            .providers
341            .iter()
342            .map(|(k, v)| (k.as_str(), v))
343            .collect(),
344    };
345
346    if entries.is_empty() {
347        println!("No QPU providers configured.");
348        println!("Run: qualia-cli --enable-qpu qpu list-providers");
349        return;
350    }
351
352    println!("================================================================");
353    println!("  QPU Configuration (API keys masked)");
354    println!("================================================================");
355
356    for (id, cfg) in &entries {
357        let name = find_provider(id).map(|p| p.name).unwrap_or(id);
358        println!("\n  [{id}]  {name}");
359        println!(
360            "  api_key              : {}",
361            display_opt(&cfg.api_key, true)
362        );
363        println!(
364            "  endpoint             : {}",
365            display_opt(&cfg.endpoint, false)
366        );
367        // IBM
368        if cfg.hub.is_some()
369            || cfg.group.is_some()
370            || cfg.project.is_some()
371            || cfg.instance.is_some()
372        {
373            println!(
374                "  hub / group / project: {} / {} / {}",
375                display_opt(&cfg.hub, false),
376                display_opt(&cfg.group, false),
377                display_opt(&cfg.project, false)
378            );
379            println!(
380                "  instance             : {}",
381                display_opt(&cfg.instance, false)
382            );
383        }
384        // Azure
385        if cfg.subscription_id.is_some() {
386            println!(
387                "  subscription_id      : {}",
388                display_opt(&cfg.subscription_id, true)
389            );
390            println!(
391                "  resource_group       : {}",
392                display_opt(&cfg.resource_group, false)
393            );
394            println!(
395                "  workspace            : {}",
396                display_opt(&cfg.workspace, false)
397            );
398            println!(
399                "  location             : {}",
400                display_opt(&cfg.location, false)
401            );
402        }
403        // Braket
404        if cfg.access_key_id.is_some() {
405            println!(
406                "  access_key_id        : {}",
407                display_opt(&cfg.access_key_id, true)
408            );
409            println!(
410                "  secret_access_key    : {}",
411                display_opt(&cfg.secret_access_key, true)
412            );
413            println!(
414                "  region               : {}",
415                display_opt(&cfg.region, false)
416            );
417            println!(
418                "  s3_bucket            : {}",
419                display_opt(&cfg.s3_bucket, false)
420            );
421        }
422        // Google
423        if cfg.project_id.is_some() {
424            println!(
425                "  project_id           : {}",
426                display_opt(&cfg.project_id, false)
427            );
428            println!(
429                "  processor_id         : {}",
430                display_opt(&cfg.processor_id, false)
431            );
432            println!(
433                "  service_account_key  : {}",
434                display_opt(&cfg.service_account_key_path, false)
435            );
436        }
437        // Rigetti
438        if cfg.user_id.is_some() || cfg.qpu_id.is_some() {
439            println!(
440                "  user_id              : {}",
441                display_opt(&cfg.user_id, false)
442            );
443            println!(
444                "  qpu_id               : {}",
445                display_opt(&cfg.qpu_id, false)
446            );
447        }
448        // IonQ
449        if cfg.backend.is_some() {
450            println!(
451                "  backend              : {}",
452                display_opt(&cfg.backend, false)
453            );
454        }
455        // Quantinuum
456        if cfg.machine.is_some() {
457            println!(
458                "  machine              : {}",
459                display_opt(&cfg.machine, false)
460            );
461        }
462    }
463    println!("\n================================================================");
464}
465
466pub fn run_clear(data_dir: &str, provider: &str) {
467    if find_provider(provider).is_none() {
468        eprintln!(
469            "Unknown provider '{}'. Run `qpu list-providers` to see valid IDs.",
470            provider
471        );
472        return;
473    }
474    let mut store = load_config(data_dir);
475    if store.providers.remove(provider).is_some() {
476        save_config(data_dir, &store);
477        println!("Cleared credentials for '{}'.", provider);
478    } else {
479        println!(
480            "No stored credentials for '{}' — nothing to clear.",
481            provider
482        );
483    }
484}
485
486pub fn run_test_connection(data_dir: &str, provider: &str) {
487    let Some(info) = find_provider(provider) else {
488        eprintln!(
489            "Unknown provider '{}'. Run `qpu list-providers` to see valid IDs.",
490            provider
491        );
492        return;
493    };
494
495    let store = load_config(data_dir);
496    let Some(cfg) = store.providers.get(provider) else {
497        eprintln!(
498            "No credentials stored for '{}'. Run `qpu configure {}` first.",
499            provider, provider
500        );
501        return;
502    };
503
504    println!("================================================================");
505    println!("  QPU Test Connection — {}", info.name);
506    println!("================================================================");
507
508    // Check required fields
509    let missing: Vec<&str> = info
510        .required
511        .iter()
512        .copied()
513        .filter(|&field| match field {
514            "api_key" => cfg.api_key.is_none(),
515            "subscription_id" => cfg.subscription_id.is_none(),
516            "resource_group" => cfg.resource_group.is_none(),
517            "workspace" => cfg.workspace.is_none(),
518            "location" => cfg.location.is_none(),
519            "access_key_id" => cfg.access_key_id.is_none(),
520            "secret_access_key" => cfg.secret_access_key.is_none(),
521            "region" => cfg.region.is_none(),
522            "project_id" => cfg.project_id.is_none(),
523            "processor_id" => cfg.processor_id.is_none(),
524            "user_id" => cfg.user_id.is_none(),
525            _ => false,
526        })
527        .collect();
528
529    if !missing.is_empty() {
530        eprintln!("Missing required fields: {}", missing.join(", "));
531        eprintln!(
532            "Run: qualia-cli --enable-qpu qpu configure {} [--field value ...]",
533            provider
534        );
535        return;
536    }
537
538    // Provider-specific validation hints
539    let endpoint = match provider {
540        "ibm" => "https://auth.quantum-computing.ibm.com/api",
541        "dwave" => cfg
542            .endpoint
543            .as_deref()
544            .unwrap_or("https://cloud.dwavesys.com/sapi/v2"),
545        "ionq" => cfg
546            .endpoint
547            .as_deref()
548            .unwrap_or("https://api.ionq.co/v0.3"),
549        "rigetti" => cfg
550            .endpoint
551            .as_deref()
552            .unwrap_or("https://api.qcs.rigetti.com"),
553        "azure" => "https://eastus.quantum.azure.com",
554        "braket" => "https://braket.{region}.amazonaws.com (via AWS SDK)",
555        "google" => "https://quantum.googleapis.com",
556        "quantinuum" => cfg
557            .endpoint
558            .as_deref()
559            .unwrap_or("https://um.qapi.quantinuum.com"),
560        _ => "(unknown)",
561    };
562
563    println!("  Provider  : {}", info.name);
564    println!("  Endpoint  : {}", endpoint);
565    println!("  Auth type : {}", auth_type_for(provider));
566    println!();
567    println!("  Credentials : present (format not yet validated by local check)");
568    println!("  Status      : Configuration looks complete — live connectivity");
569    println!("                test requires network access and valid credentials.");
570    println!();
571    println!("  To submit a test job:");
572    println!(
573        "    qualia-cli --enable-qpu qpu submit {} --problem-type annealing --qubits 4",
574        provider
575    );
576    println!("================================================================");
577}
578
579fn auth_type_for(provider: &str) -> &'static str {
580    match provider {
581        "ibm" => "IBM token (Authorization: Bearer <api_key>)",
582        "dwave" => "Leap token (X-Auth-Token: <api_key>)",
583        "ionq" => "IonQ API key (Authorization: apiKey <api_key>)",
584        "rigetti" => "QCS credentials (api_key + user_id)",
585        "azure" => "Azure AD service principal (subscription_id / workspace)",
586        "braket" => "AWS SigV4 (access_key_id + secret_access_key)",
587        "google" => "Google service account JSON / Application Default Credentials",
588        "quantinuum" => "Quantinuum bearer token (api_key)",
589        _ => "API key",
590    }
591}
592
593pub fn run_submit(data_dir: &str, provider: &str, problem_type: &str, qubits: u32, shots: u32) {
594    let Some(info) = find_provider(provider) else {
595        eprintln!(
596            "Unknown provider '{}'. Run `qpu list-providers` to see valid IDs.",
597            provider
598        );
599        return;
600    };
601
602    let store = load_config(data_dir);
603    if store.providers.get(provider).is_none() {
604        eprintln!(
605            "No credentials for '{}'. Run `qpu configure {}` first.",
606            provider, provider
607        );
608        return;
609    }
610
611    use qualia_core_db::solvers::qpu::{JobParameters, ProblemType, QpuJob};
612
613    let pt = match problem_type {
614        "annealing" => ProblemType::Annealing,
615        "gate-model" => ProblemType::GateModel,
616        "vqe" => ProblemType::Vqe,
617        "qaoa" => ProblemType::Qaoa,
618        other => {
619            eprintln!(
620                "Unknown problem type '{}'. Use: annealing | gate-model | vqe | qaoa",
621                other
622            );
623            return;
624        }
625    };
626
627    let job_id = format!(
628        "q-{}-{}",
629        provider,
630        std::time::SystemTime::now()
631            .duration_since(std::time::UNIX_EPOCH)
632            .map(|d| d.as_millis())
633            .unwrap_or(0)
634    );
635
636    let job = QpuJob::new(
637        job_id.clone(),
638        pt,
639        JobParameters {
640            num_qubits: qubits,
641            circuit_depth: 0,
642            shots,
643            hamiltonian: Some(r#"{"J":{},"h":{}}"#.into()),
644            circuit: None,
645            extra: serde_json::json!({"provider": provider, "cli_submit": true}),
646        },
647    );
648
649    println!("================================================================");
650    println!("  QPU Job Submission — {}", info.name);
651    println!("================================================================");
652    println!("  Job ID       : {}", job_id);
653    println!("  Provider     : {} ({})", info.name, provider);
654    println!("  Problem type : {}", problem_type);
655    println!("  Qubits       : {}", qubits);
656    println!("  Shots        : {}", shots);
657    println!();
658
659    // Use the FallbackHandler for demonstration (real HTTP dispatch requires
660    // the qualia-client-core qpu_dispatcher, which needs a running daemon).
661    use qualia_core_db::solvers::qpu::dispatcher::FallbackHandler;
662    let handler = FallbackHandler::new(true);
663    match handler.simulate_classically(&job) {
664        Ok(result) => {
665            println!("  Status       : {:?}", result.status);
666            if let Some(data) = &result.result {
667                println!("  Energies     : {:?}", data.energies);
668                println!("  Measurements : {} sample(s)", data.measurements.len());
669                println!("  Metadata     : {}", data.metadata);
670            }
671            println!();
672            println!("  Note: This is a local classical simulation. To dispatch to the");
673            println!("  live {} endpoint, start the Qualia daemon:", info.name);
674            println!("    qualia-cli daemon --dev");
675            println!("  The daemon's QPU oracle handles live HTTP egress via");
676            println!("  qualia-client-core::qpu_dispatcher.");
677        }
678        Err(e) => eprintln!("Simulation error: {e}"),
679    }
680    println!("================================================================");
681}