quapp-common 0.0.12.dev13__tar.gz → 0.0.12.dev15__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.
Files changed (92) hide show
  1. {quapp_common-0.0.12.dev13/quapp_common.egg-info → quapp_common-0.0.12.dev15}/PKG-INFO +26 -4
  2. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/README.md +23 -3
  3. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/pyproject.toml +7 -2
  4. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/bridge.py +21 -16
  5. quapp_common-0.0.12.dev15/quapp_common/component/circuit_adapter.py +268 -0
  6. quapp_common-0.0.12.dev15/quapp_common/component/dispatcher.py +116 -0
  7. quapp_common-0.0.12.dev15/quapp_common/component/result_serializer.py +18 -0
  8. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/language.py +1 -0
  9. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15/quapp_common.egg-info}/PKG-INFO +26 -4
  10. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common.egg-info/SOURCES.txt +4 -1
  11. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common.egg-info/requires.txt +2 -0
  12. quapp_common-0.0.12.dev15/tests/test_circuit_adapter.py +120 -0
  13. quapp_common-0.0.12.dev15/tests/test_json_parser_utils.py +96 -0
  14. quapp_common-0.0.12.dev15/tests/test_result_serializer.py +48 -0
  15. quapp_common-0.0.12.dev13/quapp_common/component/circuit_adapter.py +0 -218
  16. quapp_common-0.0.12.dev13/quapp_common/component/dispatcher.py +0 -108
  17. quapp_common-0.0.12.dev13/quapp_common/component/result_serializer.py +0 -22
  18. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/LICENSE +0 -0
  19. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/__init__.py +0 -0
  20. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/async_tasks/__init__.py +0 -0
  21. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/async_tasks/async_invocation_task.py +0 -0
  22. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/async_tasks/async_task.py +0 -0
  23. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/async_tasks/export_circuit_task.py +0 -0
  24. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/async_tasks/post_processing_task.py +0 -0
  25. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/__init__.py +0 -0
  26. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/backend/__init__.py +0 -0
  27. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/backend/invocation.py +0 -0
  28. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/backend/job_fetcher.py +0 -0
  29. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/backend/job_fetching.py +0 -0
  30. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/backend/job_manager.py +0 -0
  31. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/callback/__init__.py +0 -0
  32. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/callback/update_job_metadata.py +0 -0
  33. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/device/__init__.py +0 -0
  34. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/component/device/device_selection.py +0 -0
  35. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/config/__init__.py +0 -0
  36. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/config/logging_config.py +0 -0
  37. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/config/thread_config.py +0 -0
  38. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/__init__.py +0 -0
  39. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/async_task/__init__.py +0 -0
  40. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/async_task/circuit_export/__init__.py +0 -0
  41. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/async_task/circuit_export/backend_holder.py +0 -0
  42. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/async_task/circuit_export/circuit_holder.py +0 -0
  43. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/backend/__init__.py +0 -0
  44. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/backend/backend_information.py +0 -0
  45. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/callback/__init__.py +0 -0
  46. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/callback/callback_url.py +0 -0
  47. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/device/__init__.py +0 -0
  48. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/device/circuit_running_option.py +0 -0
  49. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/promise/__init__.py +0 -0
  50. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/promise/post_processing_promise.py +0 -0
  51. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/promise/promise.py +0 -0
  52. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/request/__init__.py +0 -0
  53. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/request/invocation_request.py +0 -0
  54. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/request/job_fetching_request.py +0 -0
  55. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/request/request.py +0 -0
  56. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/response/__init__.py +0 -0
  57. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/response/authentication.py +0 -0
  58. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/response/custom_header.py +0 -0
  59. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/data/response/job_response.py +0 -0
  60. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/__init__.py +0 -0
  61. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/base_enum.py +0 -0
  62. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/http_header.py +0 -0
  63. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/invocation_step.py +0 -0
  64. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/media_type.py +0 -0
  65. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/processing_unit.py +0 -0
  66. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/provider_tag.py +0 -0
  67. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/sdk.py +0 -0
  68. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/status/__init__.py +0 -0
  69. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/status/job_status.py +0 -0
  70. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/status/status_code.py +0 -0
  71. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/enum/token_type.py +0 -0
  72. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/factory/__init__.py +0 -0
  73. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/factory/device_factory.py +0 -0
  74. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/factory/handler_factory.py +0 -0
  75. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/factory/provider_factory.py +0 -0
  76. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/handler/__init__.py +0 -0
  77. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/handler/handler.py +0 -0
  78. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/model/__init__.py +0 -0
  79. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/model/device/__init__.py +0 -0
  80. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/model/device/custom_device.py +0 -0
  81. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/model/device/device.py +0 -0
  82. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/model/invocation.py +0 -0
  83. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/model/provider/__init__.py +0 -0
  84. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/model/provider/provider.py +0 -0
  85. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/util/__init__.py +0 -0
  86. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/util/file_utils.py +0 -0
  87. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/util/http_utils.py +0 -0
  88. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/util/json_parser_utils.py +0 -0
  89. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common/util/response_utils.py +0 -0
  90. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common.egg-info/dependency_links.txt +0 -0
  91. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/quapp_common.egg-info/top_level.txt +0 -0
  92. {quapp_common-0.0.12.dev13 → quapp_common-0.0.12.dev15}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.12.dev13
3
+ Version: 0.0.12.dev15
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -29,6 +29,8 @@ Requires-Dist: qibo
29
29
  Requires-Dist: dimod
30
30
  Requires-Dist: qulacs
31
31
  Requires-Dist: qiskit
32
+ Requires-Dist: pytket
33
+ Requires-Dist: pennylane
32
34
  Provides-Extra: dev
33
35
  Requires-Dist: black; extra == "dev"
34
36
  Requires-Dist: bumpver; extra == "dev"
@@ -75,10 +77,12 @@ responses while work continues in the background.
75
77
  scheduler.
76
78
  - Safety: updates to jobs already DONE/FAILED are ignored to prevent
77
79
  post-completion mutations.
78
- - Multi-language SDK support (v0.0.12.dev1):
79
- - Language enum for validating and resolving Python/JavaScript runtimes.
80
+ - Multi-language SDK support:
81
+ - Language enum for validating and resolving Python, JavaScript, and Q#
82
+ runtimes.
80
83
  - SubprocessDispatcher to delegate tasks to non-Python runtimes via
81
- subprocess with timeout management and JSON payload processing.
84
+ subprocess with timeout management and JSON payload processing; supports
85
+ Node.js and `dotnet run` (Q#) runners.
82
86
  - SubprocessBridge integrating dispatcher, circuit adapter, and result
83
87
  serializer for seamless JS-Python handler interaction.
84
88
  - CircuitAdapter for converting JS subprocess circuit outputs into native
@@ -103,6 +107,24 @@ Notes:
103
107
 
104
108
  ## Recently Changes Highlights
105
109
 
110
+ ### v0.0.12.dev15
111
+ - Extend multi-language SDK support with Q# (QSharp) runtime:
112
+ - Add `QSHARP` to `Language` enum.
113
+ - Add Q# runner configuration to `SubprocessDispatcher` (`dotnet run --project .`, 300 s timeout).
114
+
115
+ ### v0.0.12.dev14
116
+ - Fix: `CircuitAdapter._adapt_openqasm` for `Sdk.PENNYLANE` now catches the
117
+ "Failed to load the qasm plugin" error raised by `qml.from_qasm` on
118
+ PennyLane versions that route QASM parsing through the IO plugin registry
119
+ and require `pennylane-qiskit` to be installed (affects both older releases
120
+ and the current latest). When the error is detected, it falls back to
121
+ parsing the QASM string via Qiskit (a hard dependency) and emitting native
122
+ PennyLane operations gate-by-gate through the new
123
+ `CircuitAdapter._qiskit_to_pennylane` helper. Supported gates: H, X, Y, Z,
124
+ S, Sdg (→ PhaseShift −π/2), T, Tdg (→ PhaseShift −π/4), CX, CZ, SWAP,
125
+ RX, RY, RZ, P/U1 (→ PhaseShift), U2, U3/U; unrecognized gates are logged
126
+ as a warning and skipped.
127
+
106
128
  ### v0.0.12.dev13
107
129
  - Fix: `CircuitAdapter._adapt_openqasm` for `Sdk.PENNYLANE` now injects string
108
130
  sentinels (`"Expectation"`, `"Variance"`, `"Sample"`, `"Probability"`,
@@ -36,10 +36,12 @@ responses while work continues in the background.
36
36
  scheduler.
37
37
  - Safety: updates to jobs already DONE/FAILED are ignored to prevent
38
38
  post-completion mutations.
39
- - Multi-language SDK support (v0.0.12.dev1):
40
- - Language enum for validating and resolving Python/JavaScript runtimes.
39
+ - Multi-language SDK support:
40
+ - Language enum for validating and resolving Python, JavaScript, and Q#
41
+ runtimes.
41
42
  - SubprocessDispatcher to delegate tasks to non-Python runtimes via
42
- subprocess with timeout management and JSON payload processing.
43
+ subprocess with timeout management and JSON payload processing; supports
44
+ Node.js and `dotnet run` (Q#) runners.
43
45
  - SubprocessBridge integrating dispatcher, circuit adapter, and result
44
46
  serializer for seamless JS-Python handler interaction.
45
47
  - CircuitAdapter for converting JS subprocess circuit outputs into native
@@ -64,6 +66,24 @@ Notes:
64
66
 
65
67
  ## Recently Changes Highlights
66
68
 
69
+ ### v0.0.12.dev15
70
+ - Extend multi-language SDK support with Q# (QSharp) runtime:
71
+ - Add `QSHARP` to `Language` enum.
72
+ - Add Q# runner configuration to `SubprocessDispatcher` (`dotnet run --project .`, 300 s timeout).
73
+
74
+ ### v0.0.12.dev14
75
+ - Fix: `CircuitAdapter._adapt_openqasm` for `Sdk.PENNYLANE` now catches the
76
+ "Failed to load the qasm plugin" error raised by `qml.from_qasm` on
77
+ PennyLane versions that route QASM parsing through the IO plugin registry
78
+ and require `pennylane-qiskit` to be installed (affects both older releases
79
+ and the current latest). When the error is detected, it falls back to
80
+ parsing the QASM string via Qiskit (a hard dependency) and emitting native
81
+ PennyLane operations gate-by-gate through the new
82
+ `CircuitAdapter._qiskit_to_pennylane` helper. Supported gates: H, X, Y, Z,
83
+ S, Sdg (→ PhaseShift −π/2), T, Tdg (→ PhaseShift −π/4), CX, CZ, SWAP,
84
+ RX, RY, RZ, P/U1 (→ PhaseShift), U2, U3/U; unrecognized gates are logged
85
+ as a warning and skipped.
86
+
67
87
  ### v0.0.12.dev13
68
88
  - Fix: `CircuitAdapter._adapt_openqasm` for `Sdk.PENNYLANE` now injects string
69
89
  sentinels (`"Expectation"`, `"Variance"`, `"Sample"`, `"Probability"`,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-common"
7
- version = "0.0.12.dev13"
7
+ version = "0.0.12.dev15"
8
8
  description = "Quapp common library supporting Quapp Platform for Quantum Computing"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
@@ -25,7 +25,9 @@ dependencies = [
25
25
  "qibo",
26
26
  "dimod",
27
27
  "qulacs",
28
- "qiskit"
28
+ "qiskit",
29
+ "pytket",
30
+ "pennylane"
29
31
  ]
30
32
  requires-python = ">=3.7"
31
33
 
@@ -34,3 +36,6 @@ dev = ["black", "bumpver", "isort", "pip-tools", "pytest"]
34
36
 
35
37
  [project.urls]
36
38
  Homepage = "https://citynow.asia/"
39
+
40
+ [tool.pylint.main]
41
+ source-roots = ["."]
@@ -1,19 +1,19 @@
1
- """
2
- QApp Platform Project
3
- bridge.py
4
- Copyright © CITYNOW Co. Ltd. All rights reserved.
5
- """
6
1
  # Quapp Platform Project
7
2
  # bridge.py
8
3
  # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+ from typing import Any, Dict
9
5
 
10
- from .circuit_adapter import CircuitAdapter
11
- from .dispatcher import SubprocessDispatcher
12
- from .result_serializer import ResultSerializer
6
+ from .circuit_adapter import adapt
7
+ from .dispatcher import dispatch
8
+ from .result_serializer import serialize
13
9
  from ..config.logging_config import logger
14
10
  from ..enum.language import Language
15
11
  from ..enum.sdk import Sdk
16
12
 
13
+ # Quapp Platform Project
14
+ # bridge.py
15
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
16
+
17
17
  logger = logger.bind(context=__name__)
18
18
 
19
19
 
@@ -32,7 +32,7 @@ class SubprocessBridge:
32
32
  self.sdk = sdk
33
33
  logger.debug(f"SubprocessBridge initialized | language={language.value} | sdk={sdk}")
34
34
 
35
- def processing(self, invocation_input: dict):
35
+ def processing(self, invocation_input: Dict[str, Any]):
36
36
  """Call JS handler's processing() and convert a result to a native circuit.
37
37
 
38
38
  Args:
@@ -47,9 +47,9 @@ class SubprocessBridge:
47
47
  logger.info(f"SubprocessBridge.processing: dispatching to {self.language.value} | sdk={self.sdk}")
48
48
  logger.debug(f"SubprocessBridge.processing: input_keys={list(invocation_input.keys())}")
49
49
 
50
- result = SubprocessDispatcher.dispatch(self.language, {
50
+ result = dispatch(self.language, {
51
51
  "action": "processing",
52
- "input" : invocation_input
52
+ "input": invocation_input
53
53
  })
54
54
 
55
55
  status = result.get("status")
@@ -62,11 +62,15 @@ class SubprocessBridge:
62
62
  payload = result.get("result")
63
63
  if payload is None:
64
64
  raise RuntimeError("JS processing returned no 'result' field")
65
+ if not isinstance(payload, dict):
66
+ raise RuntimeError(
67
+ f"JS processing returned invalid 'result' type: {type(payload).__name__}"
68
+ )
65
69
 
66
70
  fmt = payload.get("format")
67
71
  logger.debug(f"SubprocessBridge.processing: JS returned format={fmt}; adapting for sdk={self.sdk}")
68
72
 
69
- circuit = CircuitAdapter.adapt(self.sdk, payload)
73
+ circuit = adapt(self.sdk, payload)
70
74
  logger.info(f"SubprocessBridge.processing: circuit adapted successfully | type={type(circuit).__name__}")
71
75
  return circuit
72
76
 
@@ -85,11 +89,12 @@ class SubprocessBridge:
85
89
  logger.info(f"SubprocessBridge.post_processing: dispatching to {self.language.value}")
86
90
  logger.debug(f"SubprocessBridge.post_processing: job_result type={type(job_result).__name__}")
87
91
 
88
- serialized = ResultSerializer.serialize(job_result)
89
- logger.debug(f"SubprocessBridge.post_processing: serialized result keys={list(serialized.keys()) if isinstance(serialized, dict) else type(serialized).__name__}")
92
+ serialized = serialize(job_result)
93
+ logger.debug(
94
+ f"SubprocessBridge.post_processing: serialized result keys={list(serialized.keys()) if isinstance(serialized, dict) else type(serialized).__name__}")
90
95
 
91
- result = SubprocessDispatcher.dispatch(self.language, {
92
- "action" : "post_processing",
96
+ result = dispatch(self.language, {
97
+ "action": "post_processing",
93
98
  "job_result": serialized
94
99
  })
95
100
 
@@ -0,0 +1,268 @@
1
+ # Quapp Platform Project
2
+ # circuit_adapter.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ import importlib
6
+ import re
7
+ from typing import Any, Dict
8
+
9
+ import numpy as np
10
+ import pennylane as qml
11
+ import pennylane.measurements as _plm
12
+ from dimod import BinaryQuadraticModel
13
+ from pytket.qasm import circuit_from_qasm_str
14
+ from qibo.models import Circuit
15
+ from qiskit import QuantumCircuit as QiskitCircuit
16
+ from qulacs import QuantumCircuit as QulacsCircuit
17
+ from qulacs.gate import DenseMatrix as DenseMatrixGate
18
+
19
+ from ..config.logging_config import logger
20
+ from ..enum.sdk import Sdk
21
+
22
+ logger = logger.bind(context=__name__)
23
+
24
+
25
+ def adapt(sdk: Sdk, result: dict):
26
+ """Route to the appropriate adapter based on the 'format' field."""
27
+ fmt = result.get("format")
28
+ logger.debug(f"adapt() called | sdk={sdk} | format={fmt}")
29
+ if fmt == "openqasm":
30
+ circuit = result.get("circuit")
31
+ if circuit is None:
32
+ raise ValueError("Missing 'circuit' field in openqasm response")
33
+ return _adapt_openqasm(sdk, circuit)
34
+ if fmt == "qubo":
35
+ qubo = result.get("qubo")
36
+ if qubo is None:
37
+ raise ValueError("Missing 'qubo' field in qubo response")
38
+ return _adapt_qubo(sdk, qubo)
39
+ if fmt == "json":
40
+ logger.debug("Returning raw JSON data pass-through")
41
+ return result.get("data")
42
+ raise ValueError(f"Unsupported circuit interchange format: {fmt}")
43
+
44
+
45
+ def _adapt_openqasm(sdk, qasm_str):
46
+ """Convert an OpenQASM 2.0 string to a native SDK circuit object."""
47
+ logger.debug(f"Adapting OpenQASM circuit for SDK: {sdk} | qasm_length={len(qasm_str)}")
48
+ _handlers = {
49
+ Sdk.QISKIT: _openqasm_to_qiskit,
50
+ Sdk.BRAKET: _openqasm_to_braket,
51
+ Sdk.PENNYLANE: _openqasm_to_pennylane,
52
+ Sdk.PYQUIL: _openqasm_to_pyquil,
53
+ Sdk.QIBO: _openqasm_to_qibo,
54
+ Sdk.QULACS: _openqasm_to_qulacs,
55
+ Sdk.CUDA_QUANTUM: _openqasm_to_cuda_quantum,
56
+ }
57
+ handler = _handlers.get(sdk)
58
+ if handler is None:
59
+ raise ValueError(f"OpenQASM adaptation not supported for SDK: {sdk}")
60
+ return handler(qasm_str)
61
+
62
+
63
+ def _adapt_qubo(sdk, qubo_raw: Dict[str, Any]):
64
+ """Convert a QUBO dict with string keys ('x0,x1') to a BinaryQuadraticModel.
65
+
66
+ JS returns keys like "x0,x1" as strings. We parse them into tuple keys
67
+ for dimod's from_qubo() method.
68
+ """
69
+ logger.debug(f"Adapting QUBO for SDK: {sdk} | num_entries={len(qubo_raw)}")
70
+
71
+ qubo_dict = {}
72
+ for key, val in qubo_raw.items():
73
+ if isinstance(key, str) and "," in key:
74
+ parts = tuple(k.strip() for k in key.split(",", 1))
75
+ qubo_dict[parts] = val
76
+ else:
77
+ qubo_dict[(key, key)] = val
78
+
79
+ logger.debug(f"QUBO parsed: {len(qubo_dict)} entries -> BinaryQuadraticModel")
80
+ return BinaryQuadraticModel.from_qubo(qubo_dict)
81
+
82
+
83
+ def _count_qasm_qubits(qasm_str):
84
+ """Sum qubit counts across all `qreg <name>[<n>];` declarations."""
85
+ return sum(int(m) for m in re.findall(r'qreg\s+\w+\[(\d+)]', qasm_str))
86
+
87
+
88
+ def _openqasm_to_braket(qasm_str):
89
+ logger.debug("Converting OpenQASM via pytket -> tk_to_braket")
90
+ tk_to_braket = importlib.import_module("pytket.extensions.braket").tk_to_braket
91
+ result = tk_to_braket(circuit_from_qasm_str(qasm_str))
92
+ # tk_to_braket returns (Circuit, n_shots) in pytket-braket >= 0.35
93
+ is_tuple = isinstance(result, tuple)
94
+ logger.debug(
95
+ f"tk_to_braket returned tuple={is_tuple}; unpacking index 0" if is_tuple else "tk_to_braket returned Circuit directly")
96
+ return result[0] if is_tuple else result
97
+
98
+
99
+ def _openqasm_to_cuda_quantum(qasm_str):
100
+ logger.debug("CUDA-Q: passing OpenQASM string through directly")
101
+ # CUDA-Q accepts OpenQASM directly via kernel builder
102
+ return qasm_str
103
+
104
+
105
+ def _openqasm_to_pennylane(qasm_str):
106
+ logger.debug("Converting OpenQASM via PennyLane qml.from_qasm wrapper")
107
+ for _compat_name in ("Expectation", "Variance", "Sample", "Probability",
108
+ "State", "MidMeasure"):
109
+ if not hasattr(_plm, _compat_name):
110
+ setattr(_plm, _compat_name, _compat_name)
111
+
112
+ num_wires = _count_qasm_qubits(qasm_str)
113
+ logger.debug(f"PennyLane: detected {num_wires} qubits; wrapping with qml.probs over all wires")
114
+ try:
115
+ base_fn = qml.from_qasm(qasm_str, measurements=[])
116
+
117
+ def _circuit():
118
+ base_fn()
119
+ if num_wires > 0:
120
+ return qml.probs(wires=list(range(num_wires)))
121
+ return None
122
+
123
+ return _circuit
124
+ except Exception as _e:
125
+ if "qasm plugin" not in str(_e) and "pennylane-qiskit" not in str(_e):
126
+ raise
127
+ logger.warning(
128
+ f"qml.from_qasm unavailable ({_e}); falling back to Qiskit bridge for PennyLane QASM conversion"
129
+ )
130
+ return _qiskit_to_pennylane(qasm_str, num_wires)
131
+
132
+
133
+ def _openqasm_to_pyquil(qasm_str):
134
+ logger.debug("Converting OpenQASM via pytket -> tk_to_pyquil")
135
+ tk_to_pyquil = importlib.import_module("pytket.extensions.pyquil").tk_to_pyquil
136
+ return tk_to_pyquil(circuit_from_qasm_str(qasm_str))
137
+
138
+
139
+ def _openqasm_to_qibo(qasm_str):
140
+ logger.debug("Converting OpenQASM via Qibo Circuit.from_qasm")
141
+ return Circuit.from_qasm(qasm_str)
142
+
143
+
144
+ def _openqasm_to_qiskit(qasm_str):
145
+ logger.debug("Converting OpenQASM via Qiskit QuantumCircuit.from_qasm_str")
146
+ return QiskitCircuit.from_qasm_str(qasm_str)
147
+
148
+
149
+ def _openqasm_to_qulacs(qasm_str):
150
+ logger.debug("Converting OpenQASM for Qulacs via Qiskit bridge")
151
+ # Try the Qiskit bridge first; fall back to raw pass-through if
152
+ # qiskit is not installed or the conversion fails.
153
+ try:
154
+ qiskit_circ = QiskitCircuit.from_qasm_str(qasm_str)
155
+ logger.debug(
156
+ f"Qiskit parsed circuit: num_qubits={qiskit_circ.num_qubits} num_gates={len(qiskit_circ.data)}")
157
+ return _qiskit_to_qulacs(qiskit_circ)
158
+ except Exception as e:
159
+ logger.warning(f"Qiskit bridge failed for Qulacs ({e}); falling back to raw OpenQASM pass-through")
160
+ return qasm_str
161
+
162
+
163
+ def _qiskit_to_pennylane(qasm_str, num_wires):
164
+ """Parse an OpenQASM 2.0 string via Qiskit and return a PennyLane callable.
165
+
166
+ Fallback for environments where qml.from_qasm is unavailable because
167
+ pennylane-qiskit is not installed (PennyLane < 0.36).
168
+ """
169
+ qiskit_circ = QiskitCircuit.from_qasm_str(qasm_str)
170
+ qubit_map = {bit: idx for idx, bit in enumerate(qiskit_circ.qubits)}
171
+ logger.debug(
172
+ f"_qiskit_to_pennylane: num_qubits={qiskit_circ.num_qubits} "
173
+ f"num_instructions={len(qiskit_circ.data)}"
174
+ )
175
+
176
+ instructions = [
177
+ (inst.operation.name,
178
+ [qubit_map[q] for q in inst.qubits],
179
+ [float(p) for p in inst.operation.params])
180
+ for inst in qiskit_circ.data
181
+ ]
182
+
183
+ _skip = frozenset({'measure', 'barrier', 'reset', 'id'})
184
+ _gate_map = {
185
+ 'h': lambda q, p: qml.Hadamard(wires=q[0]),
186
+ 'x': lambda q, p: qml.PauliX(wires=q[0]),
187
+ 'y': lambda q, p: qml.PauliY(wires=q[0]),
188
+ 'z': lambda q, p: qml.PauliZ(wires=q[0]),
189
+ 's': lambda q, p: qml.S(wires=q[0]),
190
+ 'sdg': lambda q, p: qml.PhaseShift(-np.pi / 2, wires=q[0]),
191
+ 't': lambda q, p: qml.T(wires=q[0]),
192
+ 'tdg': lambda q, p: qml.PhaseShift(-np.pi / 4, wires=q[0]),
193
+ 'cx': lambda q, p: qml.CNOT(wires=q),
194
+ 'cz': lambda q, p: qml.CZ(wires=q),
195
+ 'swap': lambda q, p: qml.SWAP(wires=q),
196
+ 'rx': lambda q, p: qml.RX(p[0], wires=q[0]),
197
+ 'ry': lambda q, p: qml.RY(p[0], wires=q[0]),
198
+ 'rz': lambda q, p: qml.RZ(p[0], wires=q[0]),
199
+ 'p': lambda q, p: qml.PhaseShift(p[0], wires=q[0]),
200
+ 'u1': lambda q, p: qml.PhaseShift(p[0], wires=q[0]),
201
+ 'u2': lambda q, p: qml.U2(p[0], p[1], wires=q[0]),
202
+ 'u3': lambda q, p: qml.U3(p[0], p[1], p[2], wires=q[0]),
203
+ 'u': lambda q, p: qml.U3(p[0], p[1], p[2], wires=q[0]),
204
+ }
205
+
206
+ def _circuit():
207
+ for name, qubits, params in instructions:
208
+ if name in _skip:
209
+ continue
210
+ gate_fn = _gate_map.get(name)
211
+ if gate_fn is not None:
212
+ gate_fn(qubits, params)
213
+ else:
214
+ logger.warning(f"PennyLane Qiskit bridge: unrecognized gate '{name}'; skipping")
215
+ if num_wires > 0:
216
+ return qml.probs(wires=list(range(num_wires)))
217
+ return None
218
+
219
+ return _circuit
220
+
221
+
222
+ def _qiskit_to_qulacs(qiskit_circ):
223
+ """Convert a Qiskit QuantumCircuit to a qulacs QuantumCircuit.
224
+
225
+ Fixed gates are mapped via named qulacs methods; parametric and
226
+ unrecognized gates fall back to DenseMatrix (unitary passthrough).
227
+ Measurement, barrier, reset, and identity instructions are skipped.
228
+ """
229
+ n = qiskit_circ.num_qubits
230
+ logger.debug(f"_qiskit_to_qulacs: num_qubits={n} num_instructions={len(qiskit_circ.data)}")
231
+ qulacs_circ = QulacsCircuit(n)
232
+ qubit_map = {bit: idx for idx, bit in enumerate(qiskit_circ.qubits)}
233
+
234
+ _SINGLE = {
235
+ 'h': 'add_H_gate', 'x': 'add_X_gate',
236
+ 'y': 'add_Y_gate', 'z': 'add_Z_gate',
237
+ 's': 'add_S_gate', 'sdg': 'add_Sdag_gate',
238
+ 't': 'add_T_gate', 'tdg': 'add_Tdag_gate',
239
+ }
240
+ _TWO = {
241
+ 'cx': 'add_CNOT_gate',
242
+ 'cz': 'add_CZ_gate',
243
+ 'swap': 'add_SWAP_gate',
244
+ }
245
+ _SKIP = {'measure', 'barrier', 'reset', 'id'}
246
+
247
+ for inst in qiskit_circ.data:
248
+ gate = inst.operation
249
+ qubits = [qubit_map[q] for q in inst.qubits]
250
+ name = gate.name
251
+
252
+ if name in _SKIP:
253
+ continue
254
+ elif name in _SINGLE:
255
+ getattr(qulacs_circ, _SINGLE[name])(qubits[0])
256
+ elif name in _TWO:
257
+ getattr(qulacs_circ, _TWO[name])(qubits[0], qubits[1])
258
+ else:
259
+ # Parametric gates (rx, ry, rz, u*, p, ...) and any
260
+ # unrecognized gate: extract the unitary and apply it
261
+ # as a DenseMatrix. Single-qubit gates have no ordering
262
+ # ambiguity; multi-qubit gates are passed with qulacs'
263
+ # index order matching Qiskit's instruction qubit order.
264
+ logger.debug(f"Gate '{name}' not in named map; converting via DenseMatrix on qubits={qubits}")
265
+ qulacs_circ.add_gate(DenseMatrixGate(qubits, gate.to_matrix()))
266
+
267
+ logger.debug(f"_qiskit_to_qulacs: conversion complete")
268
+ return qulacs_circ
@@ -0,0 +1,116 @@
1
+ # Quapp Platform Project
2
+ # dispatcher.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ import json
6
+ import os
7
+ import subprocess
8
+
9
+ from ..config.logging_config import logger
10
+ from ..enum.language import Language
11
+
12
+ logger = logger.bind(context=__name__)
13
+
14
+ WORKING_DIR: str = None
15
+
16
+ RUNNER_MAP = {
17
+ Language.JAVASCRIPT: {
18
+ 'command': ['node', 'handler_runner.js'],
19
+ 'timeout': 300,
20
+ },
21
+ Language.QSHARP: {
22
+ 'command': ['dotnet', 'run', '--project', 'HandlerRunner.csproj'],
23
+ 'timeout': 300,
24
+ },
25
+ }
26
+
27
+
28
+ def dispatch(language: Language, request_data: dict,
29
+ timeout: int = None) -> dict:
30
+ """Dispatch request_data to a subprocess runner.
31
+
32
+ Args:
33
+ language: The target language runtime.
34
+ request_data: Dict to serialize as JSON and pipe to stdin.
35
+ timeout: Optional timeout in seconds. Overrides the runner by default.
36
+
37
+ Returns:
38
+ Parsed JSON dict from the subprocess stdout.
39
+ """
40
+ runner = RUNNER_MAP.get(language)
41
+ if runner is None:
42
+ raise ValueError(f"No runner configured for language: {language}")
43
+
44
+ effective_timeout = timeout if timeout is not None else runner[
45
+ 'timeout']
46
+ action = request_data.get('action', 'handle')
47
+ logger.info(f"Dispatching to {language.value} runner, action={action}, "
48
+ f"timeout={effective_timeout}s")
49
+
50
+ payload = json.dumps(request_data)
51
+ cwd = WORKING_DIR or os.getcwd()
52
+ result = subprocess.run(runner['command'], input=payload,
53
+ capture_output=True, text=True,
54
+ timeout=effective_timeout, cwd=cwd, check=True)
55
+
56
+ if result.stderr:
57
+ logger.debug(f"Subprocess stderr: {result.stderr[:500]}")
58
+
59
+ if result.returncode != 0:
60
+ error_message = _extract_error_message(result)
61
+ logger.error(
62
+ f"Handler runner failed (exit={result.returncode}): {error_message[:500]}")
63
+ raise RuntimeError(f"Handler runner failed: {error_message}")
64
+
65
+ if not result.stdout:
66
+ raise RuntimeError("Handler runner produced no output")
67
+
68
+ try:
69
+ return json.loads(result.stdout)
70
+ except json.JSONDecodeError as exc:
71
+ raise RuntimeError(
72
+ f"Handler runner returned invalid JSON: {exc}") from exc
73
+
74
+
75
+ def is_subprocess_language(language_str: str) -> bool:
76
+ """
77
+ Determines if a given language string represents a subprocess-compatible language.
78
+
79
+ This method checks whether the specified language can be executed as a subprocess
80
+ (i.e., it is not equivalent to Python). It resolves the language using the
81
+ `Language.resolve` method and compares it to `Language.PYTHON`. If the language
82
+ is unresolvable or an exception occurs, it assumes the language is not valid for
83
+ subprocess execution.
84
+
85
+ Args:
86
+ language_str (str): The language string to be validated.
87
+
88
+ Returns:
89
+ bool: True if the language is a subprocess-compatible language, False otherwise.
90
+ """
91
+ try:
92
+ lang = Language.resolve(language_str)
93
+ return lang != Language.PYTHON
94
+ except Exception:
95
+ return False
96
+
97
+
98
+ def _extract_error_message(result) -> str:
99
+ """Extract the most meaningful error message from a failed subprocess.
100
+
101
+ Priority order:
102
+ 1. Structured JSON on stdout – {"status":"error","error":"..."}
103
+ 2. Raw stderr text
104
+ 3. Generic exit-code string
105
+ """
106
+ if result.stdout:
107
+ try:
108
+ parsed = json.loads(result.stdout)
109
+ if parsed.get('status') == 'error':
110
+ return (parsed.get('error') or parsed.get(
111
+ 'stack') or 'Unknown JS error')
112
+ except (json.JSONDecodeError, AttributeError):
113
+ pass
114
+ if result.stderr:
115
+ return result.stderr.strip()
116
+ return f"exit code {result.returncode}"
@@ -0,0 +1,18 @@
1
+ # Quapp Platform Project
2
+ # result_serializer.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+ from ..util.json_parser_utils import parse
5
+
6
+
7
+ def serialize(job_result):
8
+ """
9
+ Convert a job result (which may contain numpy arrays, complex numbers,
10
+ Qiskit/Braket objects, etc.) into a plain JSON-serializable dict.
11
+
12
+ Reuses the existing parse() utility that handles numpy, complex, datetime,
13
+ Enum, and custom objects with to_dict()/__dict__.
14
+ """
15
+ try:
16
+ return parse(job_result)
17
+ except Exception as e:
18
+ raise RuntimeError(f"Failed to serialize job result: {e}") from e
@@ -12,6 +12,7 @@ from ..enum.base_enum import BaseEnum
12
12
  class Language(BaseEnum):
13
13
  PYTHON = 'python'
14
14
  JAVASCRIPT = 'javascript'
15
+ QSHARP = 'qsharp'
15
16
 
16
17
  @staticmethod
17
18
  def resolve(language: str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-common
3
- Version: 0.0.12.dev13
3
+ Version: 0.0.12.dev15
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -29,6 +29,8 @@ Requires-Dist: qibo
29
29
  Requires-Dist: dimod
30
30
  Requires-Dist: qulacs
31
31
  Requires-Dist: qiskit
32
+ Requires-Dist: pytket
33
+ Requires-Dist: pennylane
32
34
  Provides-Extra: dev
33
35
  Requires-Dist: black; extra == "dev"
34
36
  Requires-Dist: bumpver; extra == "dev"
@@ -75,10 +77,12 @@ responses while work continues in the background.
75
77
  scheduler.
76
78
  - Safety: updates to jobs already DONE/FAILED are ignored to prevent
77
79
  post-completion mutations.
78
- - Multi-language SDK support (v0.0.12.dev1):
79
- - Language enum for validating and resolving Python/JavaScript runtimes.
80
+ - Multi-language SDK support:
81
+ - Language enum for validating and resolving Python, JavaScript, and Q#
82
+ runtimes.
80
83
  - SubprocessDispatcher to delegate tasks to non-Python runtimes via
81
- subprocess with timeout management and JSON payload processing.
84
+ subprocess with timeout management and JSON payload processing; supports
85
+ Node.js and `dotnet run` (Q#) runners.
82
86
  - SubprocessBridge integrating dispatcher, circuit adapter, and result
83
87
  serializer for seamless JS-Python handler interaction.
84
88
  - CircuitAdapter for converting JS subprocess circuit outputs into native
@@ -103,6 +107,24 @@ Notes:
103
107
 
104
108
  ## Recently Changes Highlights
105
109
 
110
+ ### v0.0.12.dev15
111
+ - Extend multi-language SDK support with Q# (QSharp) runtime:
112
+ - Add `QSHARP` to `Language` enum.
113
+ - Add Q# runner configuration to `SubprocessDispatcher` (`dotnet run --project .`, 300 s timeout).
114
+
115
+ ### v0.0.12.dev14
116
+ - Fix: `CircuitAdapter._adapt_openqasm` for `Sdk.PENNYLANE` now catches the
117
+ "Failed to load the qasm plugin" error raised by `qml.from_qasm` on
118
+ PennyLane versions that route QASM parsing through the IO plugin registry
119
+ and require `pennylane-qiskit` to be installed (affects both older releases
120
+ and the current latest). When the error is detected, it falls back to
121
+ parsing the QASM string via Qiskit (a hard dependency) and emitting native
122
+ PennyLane operations gate-by-gate through the new
123
+ `CircuitAdapter._qiskit_to_pennylane` helper. Supported gates: H, X, Y, Z,
124
+ S, Sdg (→ PhaseShift −π/2), T, Tdg (→ PhaseShift −π/4), CX, CZ, SWAP,
125
+ RX, RY, RZ, P/U1 (→ PhaseShift), U2, U3/U; unrecognized gates are logged
126
+ as a warning and skipped.
127
+
106
128
  ### v0.0.12.dev13
107
129
  - Fix: `CircuitAdapter._adapt_openqasm` for `Sdk.PENNYLANE` now injects string
108
130
  sentinels (`"Expectation"`, `"Variance"`, `"Sample"`, `"Probability"`,