quantum-flows 0.1.14__py3-none-any.whl → 0.1.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -148,7 +148,7 @@ class InputData:
148
148
  "ising-model",
149
149
  "lattice",
150
150
  "lp-model",
151
- "max-iterations",
151
+ "max-fun-evaluations",
152
152
  "molecule-info",
153
153
  "operator",
154
154
  "pub",
@@ -672,7 +672,7 @@ In case the service has been recently started please wait 5 minutes for it to be
672
672
  shots=None,
673
673
  workflow_id=None,
674
674
  comments="",
675
- max_iterations=None,
675
+ max_fun_evaluations=None,
676
676
  input_data=InputData(),
677
677
  ):
678
678
  if not self._verify_user_is_authenticated():
@@ -689,10 +689,10 @@ In case the service has been recently started please wait 5 minutes for it to be
689
689
  if not self.is_valid_uuid(workflow_id):
690
690
  print("The specified workflow Id is not a valid GUID.")
691
691
  return
692
- if max_iterations is not None:
693
- if not isinstance(max_iterations, int) or max_iterations <= 0:
692
+ if max_fun_evaluations is not None:
693
+ if not isinstance(max_fun_evaluations, int) or max_fun_evaluations <= 0:
694
694
  print(
695
- "The optional 'max-iterations' input argument must be a positive integer."
695
+ "The optional 'max-fun-evaluations' input argument must be a positive integer."
696
696
  )
697
697
  return
698
698
  try:
@@ -702,8 +702,8 @@ In case the service has been recently started please wait 5 minutes for it to be
702
702
  input_data_items.append(backend)
703
703
  input_data_labels.append("shots")
704
704
  input_data_items.append(str(shots))
705
- input_data_labels.append("max-iterations")
706
- input_data_items.append(str(max_iterations))
705
+ input_data_labels.append("max-fun-evaluations")
706
+ input_data_items.append(str(max_fun_evaluations))
707
707
  for input_data_label in input_data.data.keys():
708
708
  input_data_labels.append(input_data_label)
709
709
  content = input_data.data[input_data_label]
@@ -715,7 +715,7 @@ In case the service has been recently started please wait 5 minutes for it to be
715
715
  "WorkflowId": workflow_id,
716
716
  "Shots": shots,
717
717
  "Comments": comments,
718
- "MaxIterations": max_iterations,
718
+ "MaxIterations": max_fun_evaluations,
719
719
  "InputDataLabels": input_data_labels,
720
720
  "InputDataItems": input_data_items,
721
721
  "QiskitVersion": qiskit.__version__,
@@ -0,0 +1,966 @@
1
+ import base64
2
+ import io
3
+ import json
4
+ import numpy as np
5
+ import qiskit
6
+ import requests
7
+ import secrets
8
+ import time
9
+ import uuid
10
+ import webbrowser
11
+
12
+ from IPython.display import display, HTML
13
+ from keycloak import KeycloakOpenID
14
+ from urllib.parse import urlencode
15
+
16
+ from qiskit import qpy
17
+ from qiskit import QuantumCircuit
18
+ from qiskit.quantum_info import Operator, Pauli, PauliList, SparsePauliOp
19
+ from qiskit.quantum_info.operators.linear_op import LinearOp
20
+ from qiskit_nature.second_q.hamiltonians.lattices import (
21
+ KagomeLattice,
22
+ Lattice,
23
+ LineLattice,
24
+ HexagonalLattice,
25
+ HyperCubicLattice,
26
+ SquareLattice,
27
+ TriangularLattice,
28
+ )
29
+ from qiskit_nature.second_q.hamiltonians.lattices.boundary_condition import (
30
+ BoundaryCondition,
31
+ )
32
+ from qiskit_optimization import QuadraticProgram
33
+
34
+
35
+ class CustomJSONEncoder(json.JSONEncoder):
36
+ def default(self, obj):
37
+ if isinstance(obj, complex):
38
+ return {"real": obj.real, "imag": obj.imag}
39
+ if isinstance(obj, np.ndarray):
40
+ return obj.tolist()
41
+ if isinstance(obj, BoundaryCondition):
42
+ # Convert enum to string
43
+ return str(obj)
44
+ return super().default(obj)
45
+
46
+
47
+ def all_numbers(lst):
48
+ return all(isinstance(x, (int, float, complex)) for x in lst)
49
+
50
+
51
+ def serialize_circuit(circuit):
52
+ buffer = io.BytesIO()
53
+ qpy.dump(circuit, buffer)
54
+ qpy_binary_data = buffer.getvalue()
55
+ base64_encoded_circuit = base64.b64encode(qpy_binary_data).decode("utf-8")
56
+ return base64_encoded_circuit
57
+
58
+
59
+ class AuthenticationFailure(Exception):
60
+ def __init__(self, message):
61
+ self.message = message
62
+
63
+
64
+ class AuthorizationFailure(Exception):
65
+ def __init__(self, message):
66
+ self.message = message
67
+
68
+
69
+ class Job:
70
+ def __init__(self, job_id):
71
+ self._job_id = job_id
72
+
73
+ def id(self):
74
+ return self._job_id
75
+
76
+
77
+ class WorkflowJob:
78
+ def __init__(self, job_id):
79
+ self._job_id = job_id
80
+
81
+ def id(self):
82
+ return self._job_id
83
+
84
+
85
+ class InputData:
86
+ def __init__(self, label=None, content=None):
87
+ self.data = {}
88
+ if label:
89
+ self.add_data(label, content)
90
+
91
+ def __str__(self):
92
+ return json.dumps(self.data, indent=2, cls=CustomJSONEncoder)
93
+
94
+ def add_data(self, label, content):
95
+ self.check_label(label, self.data)
96
+ try:
97
+ if label == "operator":
98
+ operator = content
99
+ self.validate_operator(operator)
100
+ coeffs = None
101
+ if type(content) == tuple:
102
+ operator, coeffs = content
103
+ sparse_pauli_operator = self.to_sparse_pauli_operator(
104
+ operator, coeffs=coeffs
105
+ )
106
+ pauli_terms, coefficients = self.serialize_sparse_pauli_operator(
107
+ sparse_pauli_operator
108
+ )
109
+ self.data["operator"] = {
110
+ "pauli-terms": pauli_terms,
111
+ "coefficients": coefficients,
112
+ "operator-string-representation": str(operator),
113
+ }
114
+ elif label == "pub":
115
+ content = self.validate_and_serialize_pub(content)
116
+ if not "pubs" in self.data.keys():
117
+ self.data["pubs"] = []
118
+ self.data["pubs"].append(content)
119
+ elif label == "molecule-info":
120
+ self.validate_molecule_info(content)
121
+ self.data[label] = content
122
+ elif label == "lattice":
123
+ self.data[label] = self.lattice_to_dict(content)
124
+ elif label == "ising-model":
125
+ self.validate_ising_model(content)
126
+ self.data[label] = content
127
+ elif label == "training-data":
128
+ self.validate_training_data(content)
129
+ self.data[label] = content
130
+ elif label == "inference-data":
131
+ self.validate_inference_data(content)
132
+ self.data[label] = content
133
+ elif label == "quadratic-program":
134
+ self.validate_quadratic_program(content)
135
+ lp_string = content.export_as_lp_string()
136
+ self.data[label] = lp_string
137
+ else:
138
+ self.data[label] = content
139
+ except (OverflowError, TypeError, ValueError):
140
+ raise Exception("Input data content must be JSON serializable.")
141
+
142
+ def check_label(self, label, data):
143
+ if type(label) != str:
144
+ raise Exception("Input data label must be string.")
145
+ if label not in [
146
+ "ansatz-parameters",
147
+ "inference-data",
148
+ "ising-model",
149
+ "lattice",
150
+ "lp-model",
151
+ "max-iterations",
152
+ "molecule-info",
153
+ "operator",
154
+ "pub",
155
+ "quadratic-program",
156
+ "training-data",
157
+ ]:
158
+ raise Exception(
159
+ f"Input data of type {label} is not supported. Please choose one of the following options: 'ansatz-parameters', 'inference-data' 'ising-model', 'lattice', 'lp-model', 'molecule-info', 'operator', 'pub', 'training-data'."
160
+ )
161
+ if label != "pub" and label in data.keys():
162
+ raise Exception(
163
+ f"An input data item of type '{label}' has already been added to the job input data. Multiple data items of same category are allowed only for PUBs."
164
+ )
165
+
166
+ def lattice_to_dict(self, lattice):
167
+ if isinstance(lattice, LineLattice):
168
+ lattice_data = {
169
+ "type": "LineLattice",
170
+ "num_nodes": lattice.num_nodes,
171
+ "boundary_condition": lattice.boundary_condition[0].name,
172
+ "edge_parameter": lattice.edge_parameter,
173
+ "onsite_parameter": lattice.onsite_parameter,
174
+ }
175
+ return lattice_data
176
+ elif isinstance(lattice, TriangularLattice):
177
+ lattice_data = {
178
+ "type": "TriangularLattice",
179
+ "rows": lattice.rows,
180
+ "cols": lattice.cols,
181
+ "boundary_condition": lattice.boundary_condition.name,
182
+ "edge_parameter": lattice.edge_parameter,
183
+ "onsite_parameter": lattice.onsite_parameter,
184
+ }
185
+ return lattice_data
186
+ elif isinstance(lattice, (SquareLattice, KagomeLattice, HyperCubicLattice)):
187
+ lattice_data = {
188
+ "type": type(lattice).__name__,
189
+ "rows": lattice.rows,
190
+ "cols": lattice.cols,
191
+ "boundary_condition": [
192
+ bc.name
193
+ for bc in (
194
+ lattice.boundary_condition
195
+ if isinstance(lattice.boundary_condition, tuple)
196
+ else (lattice.boundary_condition,)
197
+ )
198
+ ],
199
+ "edge_parameter": lattice.edge_parameter,
200
+ "onsite_parameter": lattice.onsite_parameter,
201
+ }
202
+ return lattice_data
203
+ elif isinstance(lattice, HexagonalLattice):
204
+ lattice_data = {
205
+ "type": "HexagonalLattice",
206
+ "rows": lattice._rows,
207
+ "cols": lattice._cols,
208
+ "edge_parameter": lattice.edge_parameter,
209
+ "onsite_parameter": lattice.onsite_parameter,
210
+ }
211
+ return lattice_data
212
+ elif isinstance(lattice, Lattice):
213
+ graph = lattice.graph
214
+ nodes = list(graph.node_indexes())
215
+ edges = [
216
+ {"source": edge[0], "target": edge[1], "weight": edge[2]}
217
+ for edge in graph.weighted_edge_list()
218
+ ]
219
+ lattice_data = {
220
+ "type": "Lattice",
221
+ "nodes": nodes,
222
+ "edges": edges,
223
+ "num_nodes": lattice.num_nodes,
224
+ }
225
+ return lattice_data
226
+ else:
227
+ raise Exception(
228
+ "This input lattice object is not supported. Please use an object of the following types: Lattice, LineLattice, TriangularLattice, SquareLattice, KagomeLattice, HyperCubicLattice or HexagonalLattice. All of them are available in the qiskit_nature library."
229
+ )
230
+
231
+ def validate_molecule_info(self, molecule_info):
232
+ if not isinstance(molecule_info["symbols"], list):
233
+ raise Exception("The 'symbols' must be a list of nuclei.")
234
+ if not isinstance(molecule_info["coords"], list):
235
+ raise Exception(
236
+ "The 'coords' must be a list of tuples representing the x, y, z position of each nuclei."
237
+ )
238
+ if "mutiplicity" in molecule_info and not isinstance(
239
+ molecule_info["mutiplicity"], int
240
+ ):
241
+ raise Exception("The 'multiplicity' must be an integer.")
242
+ if "charge" in molecule_info and not isinstance(molecule_info["charge"], int):
243
+ raise Exception("The 'charge' must be an integer.")
244
+ if (
245
+ "units" in molecule_info
246
+ and molecule_info["units"].lower() != "angstrom"
247
+ and molecule_info["units"].lower() != "bohr"
248
+ ):
249
+ raise Exception("The 'units' must be either 'Angstrom' or 'Bohr'.")
250
+ if "masses" in molecule_info and not all(
251
+ isinstance(m, (int, float)) for m in molecule_info["masses"]
252
+ ):
253
+ raise Exception(
254
+ "The 'masses' must be a list of floats, one for each nucleus in the molecule."
255
+ )
256
+
257
+ def validate_ising_model(self, ising_model):
258
+ if not isinstance(ising_model, dict):
259
+ raise Exception("The 'ising_model' must be a dictionary.")
260
+
261
+ for key in ising_model.keys():
262
+ if key not in ["h", "J"]:
263
+ raise Exception(
264
+ "The 'ising_model' dictionary can only contain the keys: 'h' and 'J'."
265
+ )
266
+
267
+ if "h" in ising_model:
268
+ if not isinstance(ising_model["h"], list):
269
+ raise Exception("The 'h' field must be a list of numeric values.")
270
+ if not all(isinstance(h, (int, float)) for h in ising_model["h"]):
271
+ raise Exception("Each element in 'h' must be an int or float.")
272
+
273
+ if "J" in ising_model:
274
+ if not isinstance(ising_model["J"], list):
275
+ raise Exception("The 'J' field must be a list of dictionaries.")
276
+ for interaction in ising_model["J"]:
277
+ if not isinstance(interaction, dict):
278
+ raise Exception(
279
+ "Each item in 'J' must be a dictionary with 'pair' and 'value' keys."
280
+ )
281
+ if "pair" not in interaction or "value" not in interaction:
282
+ raise Exception(
283
+ "Each item in 'J' must contain 'pair' and 'value' keys."
284
+ )
285
+ if (
286
+ not isinstance(interaction["pair"], list)
287
+ or len(interaction["pair"]) != 2
288
+ or not all(isinstance(i, int) for i in interaction["pair"])
289
+ ):
290
+ raise Exception("'pair' must be a list of two integers.")
291
+ if not isinstance(interaction["value"], (int, float)):
292
+ raise Exception("'value' must be a numeric type (int or float).")
293
+
294
+ def validate_training_data(self, training_data):
295
+ vector_size = None
296
+ if not isinstance(training_data, list):
297
+ raise Exception("The 'training_data' must be a list of dictionaries.")
298
+ for data in training_data:
299
+ if not isinstance(data, dict):
300
+ raise Exception("The 'training_data' must be a list of dictionaries.")
301
+ if not "data-point" in data:
302
+ raise Exception(
303
+ "Each dictionary in the list 'training_data' must contain a 'data-point' key."
304
+ )
305
+ vector = data["data-point"]
306
+ data_tags = data["data-tags"] if "data-tags" in data else None
307
+ if not isinstance(vector, list):
308
+ raise Exception(
309
+ "The 'data-point' value must be a list of numeric values."
310
+ )
311
+ if data_tags is not None and not isinstance(data_tags, list):
312
+ raise Exception(
313
+ "The optional 'data-tags' value must be a list of strings."
314
+ )
315
+ if not all(isinstance(item, (int, float)) for item in vector):
316
+ raise Exception(
317
+ "The 'data-point' value must be a list of numeric values (int or float)."
318
+ )
319
+ if vector_size is None:
320
+ vector_size = len(vector)
321
+ if len(vector) != vector_size:
322
+ raise Exception(
323
+ "All 'data-point' vectors in training data entries must have the same length."
324
+ )
325
+ if data_tags is not None and len(data_tags) != vector_size:
326
+ raise Exception(
327
+ "If provided, the 'data-tags' list must have the same length as the 'data-point' vector."
328
+ )
329
+ if not "label" in data:
330
+ raise Exception(
331
+ "Each dictionary in the list of training data points must contain a 'label' key."
332
+ )
333
+ label = data["label"]
334
+ if not isinstance(label, (int, float)):
335
+ raise Exception(
336
+ "The 'label' value must be a numeric type (int or float)."
337
+ )
338
+
339
+ def validate_inference_data(self, inference_data):
340
+ vector_size = None
341
+ if not isinstance(inference_data, list):
342
+ raise Exception("The 'inference_data' must be a list of dictionaries.")
343
+ for data in inference_data:
344
+ if not isinstance(data, dict):
345
+ raise Exception("The 'inference_data' must be a list of dictionaries.")
346
+ if not "data-point" in data:
347
+ raise Exception(
348
+ "Each dictionary in the list of inference data points must contain a 'data-point' key."
349
+ )
350
+ vector = data["data-point"]
351
+ data_tags = data["data-tags"] if "data-tags" in data else None
352
+ if not isinstance(vector, list):
353
+ raise Exception(
354
+ "The 'data-point' value must be a list of numeric values."
355
+ )
356
+ if data_tags is not None and not isinstance(data_tags, list):
357
+ raise Exception(
358
+ "The optional 'data-tags' value must be a list of strings."
359
+ )
360
+ if not all(isinstance(item, (int, float)) for item in vector):
361
+ raise Exception(
362
+ "The 'data-point' value must be a list of numeric values (int or float)."
363
+ )
364
+ if vector_size is None:
365
+ vector_size = len(vector)
366
+ if len(vector) != vector_size:
367
+ raise Exception(
368
+ "All 'data-point' vectors in inference data entries must have the same length."
369
+ )
370
+ if data_tags is not None and len(data_tags) != vector_size:
371
+ raise Exception(
372
+ "If provided, the 'data-tags' list must have the same length as the 'data-point' vector."
373
+ )
374
+
375
+ def validate_quadratic_program(self, qp):
376
+ if not isinstance(qp, QuadraticProgram):
377
+ raise Exception(
378
+ "The input object must be an instance of QuadraticProgram class from Qiskit Optimization module."
379
+ )
380
+
381
+ def validate_and_serialize_pub(self, pub):
382
+ shots = None
383
+ paramaters = None
384
+ if type(pub) == QuantumCircuit:
385
+ quantum_circuit = pub
386
+ elif type(pub) != tuple:
387
+ raise Exception(
388
+ "A pub can be either a quantum circuit or a tuple containing a quantum circuit, optionally second a list of circuit parameters and optionally third a number of shots."
389
+ )
390
+ elif len(pub) == 3:
391
+ quantum_circuit, paramaters, shots = pub
392
+ elif len(pub) == 2:
393
+ quantum_circuit, paramaters = pub
394
+ elif len(pub) == 1:
395
+ quantum_circuit = pub[0]
396
+ else:
397
+ raise Exception(
398
+ "A pub can be a tuple with at most 3 elements: a quantum circuit, a list of circuit paramaters and a number of shots."
399
+ )
400
+ if shots is not None and type(shots) != int:
401
+ raise Exception(
402
+ "The 'shots' setting in a PUB must be an integer and be positioned as the third element of a tuple specifying a PUB."
403
+ )
404
+ if paramaters is not None and type(paramaters) != list:
405
+ raise Exception(
406
+ "The 'paramaters' in a PUB must be a list of numbers and be positioned as the second element of a tuple specifying a PUB."
407
+ )
408
+ if quantum_circuit.num_parameters == 0 and (
409
+ paramaters is not None and len(paramaters) != 0
410
+ ):
411
+ raise Exception(
412
+ "A circuit with zero parameters must have 'paramaters' argument 'None' or an empty list."
413
+ )
414
+ elif paramaters is not None and quantum_circuit.num_parameters != len(
415
+ paramaters
416
+ ):
417
+ raise Exception(
418
+ f"The number of paramaters for a quantum circuit {quantum_circuit.num_parameters} is different from the length {len(paramaters)} of the list of aruguments."
419
+ )
420
+ if paramaters is not None and not all(
421
+ isinstance(item, (int, float)) for item in paramaters
422
+ ):
423
+ raise Exception(
424
+ "The 'paramaters' setting in a PUB must be a list of numbers."
425
+ )
426
+
427
+ return (serialize_circuit(quantum_circuit), paramaters, shots)
428
+
429
+ def validate_operator(self, operator):
430
+ if (
431
+ not isinstance(operator, Operator)
432
+ and not isinstance(operator, Pauli)
433
+ and not isinstance(operator, SparsePauliOp)
434
+ and not (
435
+ isinstance(operator, tuple)
436
+ and isinstance(operator[0], PauliList)
437
+ and isinstance(operator[1], list)
438
+ and (operator[1] and not all_numbers(operator[1]))
439
+ )
440
+ ):
441
+ raise Exception(
442
+ "The operator must be an instance of the Operator, Pauli, SparsePauliOp class or a tuple containing a PauliList and a possible empty list of numeric coefficents."
443
+ )
444
+
445
+ if isinstance(operator, Operator):
446
+ matrix = operator.data
447
+ if not np.allclose(matrix, matrix.conj().T):
448
+ print("WARNING: The operator you supplied is not Hermitian!")
449
+
450
+ if (
451
+ isinstance(operator, tuple)
452
+ and isinstance(operator[0], PauliList)
453
+ and isinstance(operator[1], list)
454
+ ):
455
+ pauli_list = operator[0]
456
+ coefficients = operator[1]
457
+ if (
458
+ coefficients is not None
459
+ and len(coefficients) > 0
460
+ and len(pauli_list) != len(coefficients)
461
+ ):
462
+ raise Exception(
463
+ "The number of Pauli terms in the Pauli list must match the number of coefficients or list of coefficients must be empty."
464
+ )
465
+
466
+ def to_sparse_pauli_operator(self, operator, coeffs=None):
467
+ if isinstance(operator, SparsePauliOp):
468
+ return operator
469
+
470
+ elif isinstance(operator, Pauli):
471
+ return SparsePauliOp(operator)
472
+
473
+ elif isinstance(operator, PauliList):
474
+ if coeffs is not None and len(coeffs) > 0 and len(coeffs) != len(operator):
475
+ raise ValueError(
476
+ "Number of coefficients must match number of Pauli operators in PauliList"
477
+ )
478
+
479
+ coefficients = (
480
+ coeffs
481
+ if (coeffs is not None and len(coeffs) > 0)
482
+ else [1.0] * len(operator)
483
+ )
484
+ pauli_strings = [str(pauli) for pauli in operator]
485
+ return SparsePauliOp(pauli_strings, coeffs=coefficients)
486
+
487
+ elif isinstance(operator, Operator):
488
+ return SparsePauliOp.from_operator(operator)
489
+
490
+ def serialize_sparse_pauli_operator(self, sparse_op):
491
+ if not isinstance(sparse_op, SparsePauliOp):
492
+ raise ValueError("Input must be a SparsePauliOp")
493
+
494
+ pauli_data = sparse_op.to_list()
495
+ pauli_terms = [term[0] for term in pauli_data]
496
+ coefficients = [term[1] for term in pauli_data]
497
+ return pauli_terms, coefficients
498
+
499
+
500
+ class QuantumFlowsProvider:
501
+
502
+ _asp_net_port_dev = "5001"
503
+ _keycloak_port = "8080"
504
+ _client_id = "straful-client"
505
+ _realm_name = "straful-realm"
506
+ _provider_url_dev = "https://localhost"
507
+ _provider_url_prod = "https://quantum-flows.transilvania-quantum.com"
508
+ _keycloak_url_dev = "http://localhost"
509
+ _keycloak_url_prod = "https://keycloak.transilvania-quantum.com"
510
+
511
+ def __init__(self, use_https=True, debug=False):
512
+ self._use_https = use_https
513
+ self._debug = debug
514
+ self._state = None
515
+ self._access_token = None
516
+ self._refresh_token = None
517
+ self._token_expiration_time = None
518
+ self._refresh_token_expiration_time = None
519
+ self._asp_net_url = (
520
+ f"{self._provider_url_dev}:{self._asp_net_port_dev}"
521
+ if self._debug
522
+ else f"{self._provider_url_prod}"
523
+ )
524
+ self._auth_call_back_url = f"{self._asp_net_url}/auth/callback"
525
+ self._show_code_callback_url = f"{self._asp_net_url}/auth/showcode"
526
+ self._keycloak_server_url = (
527
+ f"{self._keycloak_url_dev}:{self._keycloak_port}"
528
+ if self._debug
529
+ else f"{self._keycloak_url_prod}"
530
+ )
531
+
532
+ if not self._is_server_online(self._keycloak_server_url):
533
+ raise SystemExit(
534
+ f"The service you are trying to access at: {self._asp_net_url}, is not responding. \
535
+ In case the service has been recently started please wait 5 minutes for it to become fully functional."
536
+ )
537
+
538
+ self._keycloak_openid = KeycloakOpenID(
539
+ server_url=self._keycloak_server_url,
540
+ client_id=self._client_id,
541
+ realm_name=self._realm_name,
542
+ verify=self._use_https,
543
+ )
544
+
545
+ def authenticate(self):
546
+ try:
547
+ self._access_token = None
548
+ self._refresh_token = None
549
+ self._token_expiration_time = None
550
+ self._refresh_token_expiration_time = None
551
+ self._store_state()
552
+ auth_url = self._get_authentication_url()
553
+ opened = webbrowser.open(auth_url)
554
+ if not opened:
555
+ display(
556
+ HTML(
557
+ f"<p>Please click to authenticate: "
558
+ f'<a href="{auth_url}" target="_blank">{auth_url}</a></p>'
559
+ )
560
+ )
561
+ auth_code = self._get_authentication_code()
562
+ token_response = self._keycloak_openid.token(
563
+ grant_type="authorization_code",
564
+ code=auth_code,
565
+ redirect_uri=self._auth_call_back_url,
566
+ )
567
+ self._access_token = token_response["access_token"]
568
+ self._refresh_token = token_response["refresh_token"]
569
+ self._token_expiration_time = (
570
+ time.time() + token_response["expires_in"] - 5
571
+ ) # seconds
572
+ self._refresh_token_expiration_time = (
573
+ time.time() + token_response["refresh_expires_in"] - 5
574
+ ) # seconds
575
+ print("Authentication successful.")
576
+ except AuthenticationFailure as ex:
577
+ print(ex.message)
578
+ except AuthorizationFailure as ex:
579
+ print(
580
+ "Failed to authenticate with the quantum provider. Make sure you are using the correct Gmail account."
581
+ )
582
+ if self._debug:
583
+ print("More details: ", ex.message)
584
+ except Exception as ex:
585
+ print("Failed to authenticate with the quantum provider.")
586
+ if "Connection refused" in str(ex):
587
+ print("The remote service does not respond. Please try again later.")
588
+ if self._debug:
589
+ print("Unexpected exception: ", ex)
590
+
591
+ def submit_job(
592
+ self, *, backend=None, circuit=None, circuits=None, shots=None, comments=""
593
+ ):
594
+ if not self._verify_user_is_authenticated():
595
+ return
596
+ if not backend:
597
+ print("Please specify the backend name.")
598
+ return
599
+ if circuit is None and circuits is None:
600
+ print(
601
+ "An quantum circuit to be executed or a list of quantum circuits to be executed must be specified."
602
+ )
603
+ return
604
+ if circuit is not None and circuits is not None:
605
+ print(
606
+ "You can use either 'circuit' or 'circuits' as input arguments but not both at the same time."
607
+ )
608
+ return
609
+ if circuit is not None and not isinstance(circuit, QuantumCircuit):
610
+ print(
611
+ "The 'circuit' argument must be an instance of QuantumCircuit or deriving from it."
612
+ )
613
+ return
614
+ if circuits is not None and (
615
+ not isinstance(circuits, list)
616
+ or not all(isinstance(circ, QuantumCircuit) for circ in circuits)
617
+ ):
618
+ print(
619
+ "The 'circuits' argument must be a list of QuantumCircuit instances or objects deriving from QuantumCircuit."
620
+ )
621
+ return
622
+ if shots is None:
623
+ print("Please specify the number of shots.")
624
+ return
625
+ if not isinstance(shots, int):
626
+ print("The number of shots must be specified as an integer number.")
627
+ return
628
+ try:
629
+ if circuit is not None:
630
+ job_data = {
631
+ "BackendName": backend,
632
+ "Circuit": serialize_circuit(circuit),
633
+ "Circuits": [],
634
+ "Shots": shots,
635
+ "Comments": comments,
636
+ "QiskitVersion": qiskit.__version__,
637
+ }
638
+ elif circuits is not None:
639
+ job_data = {
640
+ "BackendName": backend,
641
+ "Circuit": None,
642
+ "Circuits": [serialize_circuit(circuit) for circuit in circuits],
643
+ "Shots": shots,
644
+ "Comments": comments,
645
+ "QiskitVersion": qiskit.__version__,
646
+ }
647
+ (status_code, result) = self._make_post_request(
648
+ f"{self._asp_net_url}/api/job", job_data
649
+ )
650
+ if status_code == 201:
651
+ return Job(result["id"])
652
+ elif status_code == 401:
653
+ print(
654
+ "You are not authorized to access this service. Please try to authenticate first and make sure you have signed on on our web-site with a Google email account."
655
+ )
656
+ elif "Under Maintenance" in result:
657
+ print(
658
+ "The remote service is currently under maintenance. Please try again later."
659
+ )
660
+ else:
661
+ print(
662
+ f"Job submission has failed with http status code: {status_code}. \nRemote server response: '{result}'"
663
+ )
664
+ return Job(None)
665
+ except Exception as ex:
666
+ print(str(ex))
667
+
668
+ def submit_workflow_job(
669
+ self,
670
+ *,
671
+ backend=None,
672
+ shots=None,
673
+ workflow_id=None,
674
+ comments="",
675
+ max_iterations=None,
676
+ input_data=InputData(),
677
+ ):
678
+ if not self._verify_user_is_authenticated():
679
+ return
680
+ if not backend:
681
+ print("Please specify a backend name.")
682
+ return
683
+ if not workflow_id:
684
+ print("Please specify a workflow Id.")
685
+ return
686
+ if shots is not None and not isinstance(shots, int):
687
+ print("The optional number of shots input argument must be an integer.")
688
+ return
689
+ if not self.is_valid_uuid(workflow_id):
690
+ print("The specified workflow Id is not a valid GUID.")
691
+ return
692
+ if max_iterations is not None:
693
+ if not isinstance(max_iterations, int) or max_iterations <= 0:
694
+ print(
695
+ "The optional 'max-iterations' input argument must be a positive integer."
696
+ )
697
+ return
698
+ try:
699
+ input_data_labels = []
700
+ input_data_items = []
701
+ input_data_labels.append("backend")
702
+ input_data_items.append(backend)
703
+ input_data_labels.append("shots")
704
+ input_data_items.append(str(shots))
705
+ input_data_labels.append("max-iterations")
706
+ input_data_items.append(str(max_iterations))
707
+ for input_data_label in input_data.data.keys():
708
+ input_data_labels.append(input_data_label)
709
+ content = input_data.data[input_data_label]
710
+ input_data_items.append(
711
+ json.dumps(content, indent=2, cls=CustomJSONEncoder)
712
+ )
713
+ job_data = {
714
+ "BackendName": backend,
715
+ "WorkflowId": workflow_id,
716
+ "Shots": shots,
717
+ "Comments": comments,
718
+ "MaxIterations": max_iterations,
719
+ "InputDataLabels": input_data_labels,
720
+ "InputDataItems": input_data_items,
721
+ "QiskitVersion": qiskit.__version__,
722
+ }
723
+ (status_code, result) = self._make_post_request(
724
+ f"{self._asp_net_url}/api/workflow-job", job_data
725
+ )
726
+ if status_code == 201:
727
+ return WorkflowJob(result["id"])
728
+ elif status_code == 401:
729
+ print(
730
+ "You are not authorized to access this service. Please try to authenticate first and make sure you have signed on on our web-site with a Google email account."
731
+ )
732
+ else:
733
+ print(
734
+ f"Workflow job submission has failed with http status code: {status_code}. \nRemote server response: '{result}'"
735
+ )
736
+ return WorkflowJob(None)
737
+ except Exception as ex:
738
+ print(str(ex))
739
+
740
+ def get_backends(self):
741
+ if not self._verify_user_is_authenticated():
742
+ return
743
+ try:
744
+ response = self._make_get_request(f"{self._asp_net_url}/api/backends")
745
+ status_code = response.status_code
746
+ if status_code == 200:
747
+ backends = response.json()
748
+ for backend in backends["$values"]:
749
+ print(
750
+ backend["name"],
751
+ "-",
752
+ f"no qubits: {backend['noQubits']}",
753
+ "-",
754
+ "Online" if backend["online"] else "Offline",
755
+ )
756
+ else:
757
+ print(f"Request has failed with http status code: {status_code}.")
758
+ except Exception as ex:
759
+ print(str(ex))
760
+
761
+ def get_job_status(self, job):
762
+ if not self._verify_user_is_authenticated():
763
+ return
764
+ if job is None or job.id is None:
765
+ print("This job is not valid.")
766
+ return
767
+ if type(job) == Job:
768
+ try:
769
+ response = self._make_get_request(
770
+ f"{self._asp_net_url}/api/job/status/{job.id()}"
771
+ )
772
+ status_code = response.status_code
773
+ if status_code == 200:
774
+ print("Job status: ", response.text)
775
+ else:
776
+ print(f"Request has failed with http status code: {status_code}.")
777
+ except Exception as ex:
778
+ print(str(ex))
779
+ elif type(job) == WorkflowJob:
780
+ try:
781
+ response = self._make_get_request(
782
+ f"{self._asp_net_url}/api/workflow-job/status/{job.id()}"
783
+ )
784
+ status_code = response.status_code
785
+ if status_code == 200:
786
+ print("Job status: ", response.text)
787
+ else:
788
+ print(f"Request has failed with http status code: {status_code}.")
789
+ except Exception as ex:
790
+ print(str(ex))
791
+
792
+ def get_job_result(self, job):
793
+ if not self._verify_user_is_authenticated():
794
+ return
795
+ if job is None or job.id is None:
796
+ print("This job is not valid.")
797
+ return
798
+ if type(job) == Job:
799
+ try:
800
+ response = self._make_get_request(
801
+ f"{self._asp_net_url}/api/job/result/{job.id()}"
802
+ )
803
+ status_code = response.status_code
804
+ if status_code == 200:
805
+ print(response.text)
806
+ else:
807
+ print(f"Request has failed with http status code: {status_code}.")
808
+ except Exception as ex:
809
+ print(str(ex))
810
+ elif type(job) == WorkflowJob:
811
+ print("Operation not supported for workflow jobs.")
812
+
813
+ def _verify_user_is_authenticated(self):
814
+ if (
815
+ self._access_token is None
816
+ or self._refresh_token is None
817
+ or self._refresh_token_expiration_time is None
818
+ ):
819
+ print(
820
+ "You are not authorized to access this service. Please try to authenticate first and make sure you have signed on on our web-site with a Google email account."
821
+ )
822
+ return False
823
+ if self.is_refresh_token_expired():
824
+ print("You session timed out, you need to re-authenticate!")
825
+ return False
826
+ return True
827
+
828
+ def _make_get_request(self, api_url):
829
+ if self.is_token_expired():
830
+ self._try_refresh_tokens()
831
+ return requests.get(
832
+ api_url,
833
+ headers={"Authorization": f"Bearer {self._access_token}"},
834
+ verify=self._use_https,
835
+ )
836
+
837
+ def _make_post_request(self, api_url, data):
838
+ if self.is_token_expired():
839
+ self._try_refresh_tokens()
840
+ response = requests.post(
841
+ api_url,
842
+ json=data,
843
+ headers={"Authorization": f"Bearer {self._access_token}"},
844
+ verify=self._use_https,
845
+ )
846
+ try:
847
+ json = response.json()
848
+ return (response.status_code, json)
849
+ except:
850
+ return (response.status_code, response.text)
851
+
852
+ def is_token_expired(self):
853
+ if self._token_expiration_time is None:
854
+ return True
855
+ return time.time() > self._token_expiration_time
856
+
857
+ def is_refresh_token_expired(self):
858
+ if self._refresh_token_expiration_time is None:
859
+ return True
860
+ return time.time() > self._refresh_token_expiration_time
861
+
862
+ def _try_refresh_tokens(self):
863
+ try:
864
+ token_response = self._keycloak_openid.token(
865
+ grant_type="refresh_token", refresh_token=self._refresh_token
866
+ )
867
+ self._access_token = token_response["access_token"]
868
+ self._refresh_token = token_response["refresh_token"]
869
+ self._token_expiration_time = (
870
+ time.time() + token_response["expires_in"] - 5
871
+ ) # seconds
872
+ self._refresh_token_expiration_time = (
873
+ time.time() + token_response["refresh_expires_in"] - 5
874
+ ) # seconds
875
+ except:
876
+ pass
877
+
878
+ def _is_server_online(self, url):
879
+ try:
880
+ response = requests.get(url, verify=self._use_https)
881
+ if response.status_code == 200:
882
+ return True
883
+ return False
884
+ except requests.exceptions.RequestException as e:
885
+ return False
886
+
887
+ def _is_server_under_maintenance(self, url):
888
+ try:
889
+ response = requests.get(url, verify=self._use_https)
890
+ if "Under Maintenance" in response.text:
891
+ return True
892
+ return False
893
+ except requests.exceptions.RequestException as e:
894
+ return False
895
+
896
+ def _store_state(self):
897
+ state = secrets.token_urlsafe(64)
898
+ response = requests.post(
899
+ f"{self._asp_net_url}/auth/storestate",
900
+ json={"state": state},
901
+ verify=self._use_https,
902
+ )
903
+ if response.status_code != 200:
904
+ if not self._is_server_online(self._asp_net_url):
905
+ raise AuthenticationFailure(
906
+ f"The service you are trying to access at: {self._asp_net_url} is not online."
907
+ )
908
+ elif self._is_server_under_maintenance(self._asp_net_url):
909
+ raise AuthenticationFailure(
910
+ f"The service you are trying to access at: {self._asp_net_url} is under maintenance."
911
+ )
912
+ else:
913
+ raise AuthenticationFailure(
914
+ "Cannot initiate authentication, the authentication provider does not respond."
915
+ )
916
+ self._state = state
917
+
918
+ def _get_authentication_url(self):
919
+ auth_url_params = {
920
+ "client_id": self._client_id,
921
+ "redirect_uri": self._auth_call_back_url,
922
+ "response_type": "code",
923
+ "scope": "openid profile email",
924
+ "kc_idp_hint": "google",
925
+ "state": self._state,
926
+ }
927
+ return f"{self._keycloak_server_url}/realms/{self._realm_name}/protocol/openid-connect/auth?{urlencode(auth_url_params)}"
928
+
929
+ def _get_authentication_code(self):
930
+
931
+ timeout_seconds = 16
932
+ start_time = time.time()
933
+
934
+ try:
935
+ while (time.time() - start_time) < timeout_seconds:
936
+ response = requests.get(
937
+ self._show_code_callback_url,
938
+ params={"state": self._state},
939
+ verify=self._use_https,
940
+ )
941
+ # TODO: what if I use a wrong email account
942
+ if response.status_code == 400:
943
+ if response.text == "Authorization state is missing.":
944
+ raise Exception()
945
+ time.sleep(1)
946
+ continue
947
+ data = response.text
948
+ auth_code = data.split(": ")[1]
949
+ return auth_code
950
+ except Exception as e:
951
+ if self._debug:
952
+ print(
953
+ f"Failed to retrieve the authorization code from the authentication provider: {e}"
954
+ )
955
+ pass
956
+
957
+ raise AuthorizationFailure(
958
+ "Authorization code was not received. Please make sure you are using a Google account which you have signed-on our web-site. If our website is not online please try again later."
959
+ )
960
+
961
+ def is_valid_uuid(self, value: str) -> bool:
962
+ try:
963
+ uuid_obj = uuid.UUID(value)
964
+ return str(uuid_obj) == value.lower()
965
+ except (ValueError, TypeError):
966
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: quantum-flows
3
- Version: 0.1.14
3
+ Version: 0.1.15
4
4
  Summary: A python library for interacting with Transilvania-Quantum Quantum Flows quantum computing API backbone.
5
5
  License: MIT
6
6
  Author: Radu Marginean
@@ -0,0 +1,7 @@
1
+ quantum_flows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ quantum_flows/quantum_flows.py,sha256=gupYjqTeNm-0hVHc9oH2Udc7bCgymH5ed-MJjFkrpmA,39224
3
+ quantum_flows/quantum_flows.py.bck,sha256=ak5TNi4uCzuUPJn_GAQpPoH_XfaVvp85C844d8xeFbA,39179
4
+ quantum_flows-0.1.15.dist-info/LICENSE,sha256=IMxjvLtrzufAKrJSQ5u9vkcL3LbqJnOU8He5HXDet3Q,1077
5
+ quantum_flows-0.1.15.dist-info/METADATA,sha256=U3WNKJ6CPdoDR3PuGJJKMcieP36oMdQCmsOX7IzhnM4,1063
6
+ quantum_flows-0.1.15.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
7
+ quantum_flows-0.1.15.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- quantum_flows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- quantum_flows/quantum_flows.py,sha256=ak5TNi4uCzuUPJn_GAQpPoH_XfaVvp85C844d8xeFbA,39179
3
- quantum_flows-0.1.14.dist-info/LICENSE,sha256=IMxjvLtrzufAKrJSQ5u9vkcL3LbqJnOU8He5HXDet3Q,1077
4
- quantum_flows-0.1.14.dist-info/METADATA,sha256=FKmmrt9ASrWUjbIxx7GE4eBSI9OFrQSdeB1RJhajOJQ,1063
5
- quantum_flows-0.1.14.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
6
- quantum_flows-0.1.14.dist-info/RECORD,,