1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10const CONFIG_FILE: &str = "qpu_config.json";
11
12struct 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct ProviderConfig {
99 pub api_key: Option<String>,
101 pub endpoint: Option<String>,
102 pub hub: Option<String>,
104 pub group: Option<String>,
105 pub project: Option<String>,
106 pub instance: Option<String>,
107 pub subscription_id: Option<String>,
109 pub resource_group: Option<String>,
110 pub workspace: Option<String>,
111 pub location: Option<String>,
112 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 pub project_id: Option<String>,
119 pub processor_id: Option<String>,
120 pub service_account_key_path: Option<String>,
121 pub user_id: Option<String>,
123 pub qpu_id: Option<String>,
124 pub backend: Option<String>,
126 pub machine: Option<String>,
128}
129
130#[derive(Debug, Default, Serialize, Deserialize)]
131pub struct QpuConfigStore {
132 pub providers: HashMap<String, ProviderConfig>,
133}
134
135fn 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
172fn 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
190pub 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 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 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 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 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 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 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 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 if cfg.backend.is_some() {
450 println!(
451 " backend : {}",
452 display_opt(&cfg.backend, false)
453 );
454 }
455 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 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 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 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}