quantum-flows 0.1.1__tar.gz

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