quapp-merlin 0.0.1.dev1__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 (40) hide show
  1. quapp_merlin-0.0.1.dev1/LICENSE +8 -0
  2. quapp_merlin-0.0.1.dev1/PKG-INFO +138 -0
  3. quapp_merlin-0.0.1.dev1/README.md +107 -0
  4. quapp_merlin-0.0.1.dev1/pyproject.toml +43 -0
  5. quapp_merlin-0.0.1.dev1/quapp_merlin/__init__.py +8 -0
  6. quapp_merlin-0.0.1.dev1/quapp_merlin/component/__init__.py +0 -0
  7. quapp_merlin-0.0.1.dev1/quapp_merlin/component/backend/__init__.py +0 -0
  8. quapp_merlin-0.0.1.dev1/quapp_merlin/component/backend/merlin_invocation.py +82 -0
  9. quapp_merlin-0.0.1.dev1/quapp_merlin/data/__init__.py +0 -0
  10. quapp_merlin-0.0.1.dev1/quapp_merlin/data/merlin_job_spec.py +93 -0
  11. quapp_merlin-0.0.1.dev1/quapp_merlin/error_rules.py +190 -0
  12. quapp_merlin-0.0.1.dev1/quapp_merlin/factory/__init__.py +0 -0
  13. quapp_merlin-0.0.1.dev1/quapp_merlin/factory/merlin_device_factory.py +42 -0
  14. quapp_merlin-0.0.1.dev1/quapp_merlin/factory/merlin_handler_factory.py +35 -0
  15. quapp_merlin-0.0.1.dev1/quapp_merlin/factory/merlin_provider_factory.py +36 -0
  16. quapp_merlin-0.0.1.dev1/quapp_merlin/handler/__init__.py +0 -0
  17. quapp_merlin-0.0.1.dev1/quapp_merlin/handler/invocation_handler.py +59 -0
  18. quapp_merlin-0.0.1.dev1/quapp_merlin/model/__init__.py +0 -0
  19. quapp_merlin-0.0.1.dev1/quapp_merlin/model/device/__init__.py +0 -0
  20. quapp_merlin-0.0.1.dev1/quapp_merlin/model/device/merlin_device.py +160 -0
  21. quapp_merlin-0.0.1.dev1/quapp_merlin/model/device/merlin_local_device.py +195 -0
  22. quapp_merlin-0.0.1.dev1/quapp_merlin/model/device/merlin_remote_device.py +164 -0
  23. quapp_merlin-0.0.1.dev1/quapp_merlin/model/provider/__init__.py +0 -0
  24. quapp_merlin-0.0.1.dev1/quapp_merlin/model/provider/quandela_merlin_provider.py +57 -0
  25. quapp_merlin-0.0.1.dev1/quapp_merlin/model/provider/quapp_merlin_provider.py +31 -0
  26. quapp_merlin-0.0.1.dev1/quapp_merlin/util/__init__.py +0 -0
  27. quapp_merlin-0.0.1.dev1/quapp_merlin/util/single_machine.py +135 -0
  28. quapp_merlin-0.0.1.dev1/quapp_merlin/util/tensors.py +127 -0
  29. quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/PKG-INFO +138 -0
  30. quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/SOURCES.txt +38 -0
  31. quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/dependency_links.txt +1 -0
  32. quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/requires.txt +9 -0
  33. quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/top_level.txt +1 -0
  34. quapp_merlin-0.0.1.dev1/setup.cfg +4 -0
  35. quapp_merlin-0.0.1.dev1/tests/test_error_logging.py +304 -0
  36. quapp_merlin-0.0.1.dev1/tests/test_error_rules.py +181 -0
  37. quapp_merlin-0.0.1.dev1/tests/test_job_spec.py +94 -0
  38. quapp_merlin-0.0.1.dev1/tests/test_lazy_sdk_imports.py +93 -0
  39. quapp_merlin-0.0.1.dev1/tests/test_single_machine.py +214 -0
  40. quapp_merlin-0.0.1.dev1/tests/test_tensors.py +158 -0
@@ -0,0 +1,8 @@
1
+ The MIT License (MIT)
2
+ Copyright © CITYNOW Co. Ltd. All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: quapp-merlin
3
+ Version: 0.0.1.dev1
4
+ Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
+ Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
+ License: The MIT License (MIT)
7
+ Copyright © CITYNOW Co. Ltd. All rights reserved.
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
+ Project-URL: Homepage, https://quapp.cloud/
15
+ Keywords: quapp,quapp-merlin,quantum,photonic,pytorch
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Requires-Python: <3.15,>=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: quapp-common==0.0.15.dev5
23
+ Requires-Dist: merlinquantum==0.4.1
24
+ Provides-Extra: dev
25
+ Requires-Dist: black; extra == "dev"
26
+ Requires-Dist: bumpver; extra == "dev"
27
+ Requires-Dist: isort; extra == "dev"
28
+ Requires-Dist: pip-tools; extra == "dev"
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # quapp-merlin
33
+
34
+ Quapp provider lib for [MerLin](https://merlinquantum.ai/), Quandela's photonic
35
+ quantum machine learning framework. MerLin wraps Perceval but is a separate SDK
36
+ here: a MerLin handler returns a **PyTorch model**, not a circuit.
37
+
38
+ SDK tag: `MERLIN` · providers: `QUAO_QUANTUM_SIMULATOR` (local), `QUANDELA` (cloud)
39
+
40
+ ## What a handler returns
41
+
42
+ Every other SDK on the platform has `processing()` return a circuit. MerLin has
43
+ no circuit to return -- the unit of work is a `torch.nn.Module` holding a
44
+ `merlin.QuantumLayer`, plus the batch to run it on.
45
+
46
+ ```python
47
+ import merlin as ML
48
+ import torch
49
+
50
+ from quapp_merlin import MerlinJobSpec
51
+
52
+
53
+ def processing(invocation_input: dict) -> MerlinJobSpec:
54
+ layer = ML.QuantumLayer.simple(
55
+ input_size=4,
56
+ measurement_strategy=ML.MeasurementStrategy.probs())
57
+
58
+ return MerlinJobSpec(model=layer,
59
+ inputs=torch.tensor(invocation_input['inputs']),
60
+ shots=invocation_input.get('shots'))
61
+
62
+
63
+ def post_processing(job_result: dict) -> dict:
64
+ return {'probabilities': job_result['result']['values']}
65
+ ```
66
+
67
+ Returning a bare `torch.nn.Module` also works, and the batch is then read from
68
+ `invocation_input["inputs"]`.
69
+
70
+ `shots=None` (the default) means **exact probabilities**: MerLin does not invent
71
+ a shot count, and neither does this lib.
72
+
73
+ ## One invocation, one machine
74
+
75
+ MerLin fans out along three axes, and the first one can put a job on a machine
76
+ nobody selected:
77
+
78
+ | Axis | What it does | Rule here |
79
+ |---|---|---|
80
+ | Backend | one `MerlinProcessor` binds one backend, but a handler can build a second one and route layers to it | **refused** -- the machine comes from DeviceSelection |
81
+ | Chunks | `len(X) > microbatch_size` splits a batch into several cloud jobs | `chunk_concurrency=1`, batch must fit one microbatch |
82
+ | Leaves | several `QuantumLayer`s in one model, each its own remote job | one layer per invocation |
83
+
84
+ Together these keep `future.job_ids` a single id, so `provider_job_id` stays one
85
+ value the backend can store as-is. The rules live in
86
+ `quapp_merlin/util/single_machine.py` and are locked by
87
+ `tests/test_single_machine.py`.
88
+
89
+ ## Local vs remote
90
+
91
+ | | local (`QUAO_QUANTUM_SIMULATOR`) | remote (`QUANDELA`) |
92
+ |---|---|---|
93
+ | Runs on | SLOS simulation in the container, **CPU only** | Quandela Cloud, `sim:*` or `qpu:*` |
94
+ | Training | yes, `MerlinJobSpec(training=TrainingSpec(...))` | **no** -- no gradient flows through the MerLin processor |
95
+ | Phases | COMPILATION, EXECUTION, ANALYSIS | plus POLLING while waiting on the cloud |
96
+ | Job id | none | one, from `future.job_ids` |
97
+
98
+ The image ships the CPU build of torch on purpose: the default wheel drags in
99
+ the whole CUDA runtime (~2.5 GB) for a photonic simulator that never touches a
100
+ GPU. A job asking for `ProcessingUnit.GPU` runs on CPU with a warning.
101
+
102
+ ## Result shape
103
+
104
+ `_create_job` returns a dict that is already JSON-safe -- no tensor ever reaches
105
+ `json.dumps` in a callback:
106
+
107
+ ```python
108
+ {'result': {'values': [[0.24, 0.76]], 'truncated': False, 'elements': 2},
109
+ 'shape': [1, 2],
110
+ 'batch_size': 1,
111
+ 'shots': None,
112
+ 'measurement': 'probs',
113
+ 'execution_time': 0.184,
114
+ 'provider_job_id': None,
115
+ 'histogram': {'state_0': 0.24, 'state_1': 0.76},
116
+ 'metadata': {}}
117
+ ```
118
+
119
+ `histogram` is only produced for a probability measurement of a single row: with
120
+ a batch there are as many distributions as rows, and with
121
+ `mode_expectations()` / `amplitudes()` there is no distribution over states to
122
+ bin. It is `None` in those cases, and the full output stays in `result`.
123
+
124
+ Large outputs are truncated with `truncated: True` and the real element count,
125
+ rather than silently sending the first rows as if they were everything.
126
+
127
+ ## Versions
128
+
129
+ | | |
130
+ |---|---|
131
+ | `merlinquantum` | `==0.4.1` -- 0.4 renamed the `QuantumLayer` arguments 0.3 used (`output_mapping_strategy` -> `measurement_strategy`, `ansatz` -> `builder`), so `>=` would break every handler |
132
+ | `quapp-common` | `==0.0.15.dev5` -- first release carrying `Sdk.MERLIN` |
133
+ | Python | `>=3.10` (merlinquantum), `<3.15` (perceval-quandela) |
134
+
135
+ `import quapp_merlin` needs none of torch, merlin or perceval: every SDK import
136
+ sits inside the function that uses it, so the error classifier still works in an
137
+ image where the SDK failed to install. `tests/test_lazy_sdk_imports.py` enforces
138
+ it.
@@ -0,0 +1,107 @@
1
+ # quapp-merlin
2
+
3
+ Quapp provider lib for [MerLin](https://merlinquantum.ai/), Quandela's photonic
4
+ quantum machine learning framework. MerLin wraps Perceval but is a separate SDK
5
+ here: a MerLin handler returns a **PyTorch model**, not a circuit.
6
+
7
+ SDK tag: `MERLIN` · providers: `QUAO_QUANTUM_SIMULATOR` (local), `QUANDELA` (cloud)
8
+
9
+ ## What a handler returns
10
+
11
+ Every other SDK on the platform has `processing()` return a circuit. MerLin has
12
+ no circuit to return -- the unit of work is a `torch.nn.Module` holding a
13
+ `merlin.QuantumLayer`, plus the batch to run it on.
14
+
15
+ ```python
16
+ import merlin as ML
17
+ import torch
18
+
19
+ from quapp_merlin import MerlinJobSpec
20
+
21
+
22
+ def processing(invocation_input: dict) -> MerlinJobSpec:
23
+ layer = ML.QuantumLayer.simple(
24
+ input_size=4,
25
+ measurement_strategy=ML.MeasurementStrategy.probs())
26
+
27
+ return MerlinJobSpec(model=layer,
28
+ inputs=torch.tensor(invocation_input['inputs']),
29
+ shots=invocation_input.get('shots'))
30
+
31
+
32
+ def post_processing(job_result: dict) -> dict:
33
+ return {'probabilities': job_result['result']['values']}
34
+ ```
35
+
36
+ Returning a bare `torch.nn.Module` also works, and the batch is then read from
37
+ `invocation_input["inputs"]`.
38
+
39
+ `shots=None` (the default) means **exact probabilities**: MerLin does not invent
40
+ a shot count, and neither does this lib.
41
+
42
+ ## One invocation, one machine
43
+
44
+ MerLin fans out along three axes, and the first one can put a job on a machine
45
+ nobody selected:
46
+
47
+ | Axis | What it does | Rule here |
48
+ |---|---|---|
49
+ | Backend | one `MerlinProcessor` binds one backend, but a handler can build a second one and route layers to it | **refused** -- the machine comes from DeviceSelection |
50
+ | Chunks | `len(X) > microbatch_size` splits a batch into several cloud jobs | `chunk_concurrency=1`, batch must fit one microbatch |
51
+ | Leaves | several `QuantumLayer`s in one model, each its own remote job | one layer per invocation |
52
+
53
+ Together these keep `future.job_ids` a single id, so `provider_job_id` stays one
54
+ value the backend can store as-is. The rules live in
55
+ `quapp_merlin/util/single_machine.py` and are locked by
56
+ `tests/test_single_machine.py`.
57
+
58
+ ## Local vs remote
59
+
60
+ | | local (`QUAO_QUANTUM_SIMULATOR`) | remote (`QUANDELA`) |
61
+ |---|---|---|
62
+ | Runs on | SLOS simulation in the container, **CPU only** | Quandela Cloud, `sim:*` or `qpu:*` |
63
+ | Training | yes, `MerlinJobSpec(training=TrainingSpec(...))` | **no** -- no gradient flows through the MerLin processor |
64
+ | Phases | COMPILATION, EXECUTION, ANALYSIS | plus POLLING while waiting on the cloud |
65
+ | Job id | none | one, from `future.job_ids` |
66
+
67
+ The image ships the CPU build of torch on purpose: the default wheel drags in
68
+ the whole CUDA runtime (~2.5 GB) for a photonic simulator that never touches a
69
+ GPU. A job asking for `ProcessingUnit.GPU` runs on CPU with a warning.
70
+
71
+ ## Result shape
72
+
73
+ `_create_job` returns a dict that is already JSON-safe -- no tensor ever reaches
74
+ `json.dumps` in a callback:
75
+
76
+ ```python
77
+ {'result': {'values': [[0.24, 0.76]], 'truncated': False, 'elements': 2},
78
+ 'shape': [1, 2],
79
+ 'batch_size': 1,
80
+ 'shots': None,
81
+ 'measurement': 'probs',
82
+ 'execution_time': 0.184,
83
+ 'provider_job_id': None,
84
+ 'histogram': {'state_0': 0.24, 'state_1': 0.76},
85
+ 'metadata': {}}
86
+ ```
87
+
88
+ `histogram` is only produced for a probability measurement of a single row: with
89
+ a batch there are as many distributions as rows, and with
90
+ `mode_expectations()` / `amplitudes()` there is no distribution over states to
91
+ bin. It is `None` in those cases, and the full output stays in `result`.
92
+
93
+ Large outputs are truncated with `truncated: True` and the real element count,
94
+ rather than silently sending the first rows as if they were everything.
95
+
96
+ ## Versions
97
+
98
+ | | |
99
+ |---|---|
100
+ | `merlinquantum` | `==0.4.1` -- 0.4 renamed the `QuantumLayer` arguments 0.3 used (`output_mapping_strategy` -> `measurement_strategy`, `ansatz` -> `builder`), so `>=` would break every handler |
101
+ | `quapp-common` | `==0.0.15.dev5` -- first release carrying `Sdk.MERLIN` |
102
+ | Python | `>=3.10` (merlinquantum), `<3.15` (perceval-quandela) |
103
+
104
+ `import quapp_merlin` needs none of torch, merlin or perceval: every SDK import
105
+ sits inside the function that uses it, so the error classifier still works in an
106
+ image where the SDK failed to install. `tests/test_lazy_sdk_imports.py` enforces
107
+ it.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel==0.45.1"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "quapp-merlin"
7
+ version = "0.0.1.dev1"
8
+ description = "Quapp common library supporting Quapp Platform for Quantum Computing"
9
+ readme = "README.md"
10
+ authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
11
+ license = { file = "LICENSE" }
12
+ classifiers = [
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3",
16
+ ]
17
+ keywords = ["quapp", "quapp-merlin", "quantum", "photonic", "pytorch"]
18
+ # merlinquantum 0.4 renamed the QuantumLayer arguments that 0.3 used
19
+ # (output_mapping_strategy -> measurement_strategy, ansatz -> builder), so `>=`
20
+ # would silently break every handler. Exact pin, same policy as the other libs.
21
+ #
22
+ # torch is NOT listed here on purpose: the base image installs the CPU wheel
23
+ # from download.pytorch.org, and naming it here would let pip pull the default
24
+ # wheel instead, which drags in the whole CUDA runtime (~2.5 GB) for a photonic
25
+ # QPU that never touches a GPU. merlinquantum already declares torch, so the
26
+ # constraint is still enforced -- it is just satisfied by what the image
27
+ # installed first.
28
+ dependencies = [
29
+ # 0.0.15.dev5 is the first release carrying Sdk.MERLIN.
30
+ "quapp-common==0.0.15.dev5",
31
+ "merlinquantum==0.4.1"
32
+ ]
33
+ # merlinquantum requires >=3.10; perceval-quandela caps at <3.15.
34
+ requires-python = ">=3.10,<3.15"
35
+
36
+ [project.optional-dependencies]
37
+ dev = ["black", "bumpver", "isort", "pip-tools", "pytest"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://quapp.cloud/"
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["quapp_merlin*"]
@@ -0,0 +1,8 @@
1
+ from .data.merlin_job_spec import MerlinJobSpec, TrainingSpec
2
+ from .factory.merlin_handler_factory import MerlinHandlerFactory
3
+
4
+ # Imported for the side effect: register_rules() has to run before the
5
+ # first job fails, so this import cannot be made lazy.
6
+ from . import error_rules as _error_rules # noqa: E402,F401
7
+
8
+ __all__ = ['MerlinJobSpec', 'TrainingSpec', 'MerlinHandlerFactory']
@@ -0,0 +1,82 @@
1
+ # Quapp Platform Project
2
+ # merlin_invocation.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """Wiring MerLin into the invocation pipeline.
6
+
7
+ The pipeline carries whatever ``processing()`` returned without looking at it --
8
+ except in one place, ``_get_qubit_amount``, which DeviceSelection uses to pick a
9
+ machine. That is the only spot where a MerLin job has to explain itself, and the
10
+ answer is a **mode count**, not a qubit count: see the note on the method.
11
+ """
12
+
13
+ from quapp_common.component.backend.invocation import Invocation
14
+ from quapp_common.data.request.invocation_request import InvocationRequest
15
+ from quapp_common.model.provider.provider import Provider
16
+
17
+ from ...factory.merlin_device_factory import MerlinDeviceFactory
18
+ from ...factory.merlin_provider_factory import MerlinProviderFactory
19
+ from ...util.single_machine import quantum_leaves
20
+
21
+
22
+ class MerlinInvocation(Invocation):
23
+
24
+ def __init__(self, request_data: InvocationRequest):
25
+ super().__init__(request_data)
26
+
27
+ def _export_circuit(self, circuit):
28
+ # Only QISKIT and BRAKET export circuits (EXPORT_CIRCUIT_SDK in
29
+ # quapp-common). A MerLin job is a torch module, not a drawable circuit.
30
+ return None
31
+
32
+ def _create_provider(self):
33
+ self.logger.debug(
34
+ f'Creating provider: '
35
+ f'provider_tag={getattr(self.backend_information, "provider_tag", None)}')
36
+ provider = MerlinProviderFactory.create_provider(
37
+ provider_type=self.backend_information.provider_tag,
38
+ sdk=self.sdk,
39
+ authentication=self.backend_information.authentication)
40
+ self.logger.info(f'Provider created: {type(provider).__name__}')
41
+ return provider
42
+
43
+ def _create_device(self, provider: Provider):
44
+ device_specification = getattr(self.backend_information, 'device_name',
45
+ None)
46
+ self.logger.debug(f'Creating device: {device_specification}')
47
+ device = MerlinDeviceFactory.create_device(
48
+ provider=provider,
49
+ device_specification=device_specification,
50
+ authentication=self.backend_information.authentication,
51
+ sdk=self.sdk)
52
+ self.logger.info(f'Device created: {type(device).__name__}')
53
+ return device
54
+
55
+ def _get_qubit_amount(self, circuit):
56
+ """How many optical **modes** the job needs.
57
+
58
+ DeviceSelection asks for a qubit count because every other SDK is
59
+ gate-based. Photonic hardware is sized in modes, and MerLin's layers
60
+ carry a Perceval circuit whose ``m`` is exactly that. Reporting modes
61
+ here is the closest true answer; a device catalogue that stores qubits
62
+ for a Quandela machine has to be read the same way.
63
+
64
+ Returns 0 rather than raising when the model hides its circuit: failing
65
+ device selection over a metric nobody uses downstream would turn a
66
+ runnable job into a failed one.
67
+ """
68
+ for _, leaf in quantum_leaves(getattr(circuit, 'model', circuit)):
69
+ for attribute in ('m', 'n_modes', 'modes'):
70
+ modes = getattr(leaf, attribute, None)
71
+ if isinstance(modes, int) and modes > 0:
72
+ return modes
73
+
74
+ inner = getattr(leaf, 'circuit', None)
75
+ modes = getattr(inner, 'm', None)
76
+ if isinstance(modes, int) and modes > 0:
77
+ return modes
78
+
79
+ self.logger.warning(
80
+ 'Could not read a mode count from the model; reporting 0 to '
81
+ 'device selection.')
82
+ return 0
File without changes
@@ -0,0 +1,93 @@
1
+ # Quapp Platform Project
2
+ # merlin_job_spec.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """What a MerLin handler returns instead of a circuit.
6
+
7
+ Every other SDK in the platform has ``processing()`` return a circuit, and the
8
+ whole pipeline carries that object around opaquely. MerLin has no circuit to
9
+ return: the unit of work is a ``torch.nn.Module`` (normally one holding an
10
+ ``merlin.QuantumLayer``) plus the input tensor to run it on.
11
+
12
+ Returning a bare Module would not be enough -- the model alone says nothing about
13
+ WHAT to run it on, and the device would have to guess which key of
14
+ ``invocation_input`` holds the data. A spec makes the contract explicit and lets
15
+ the device reject a malformed job with a specific error code instead of a
16
+ ``TypeError`` deep inside torch.
17
+
18
+ A bare Module is still accepted as shorthand; see ``coerce()``.
19
+ """
20
+
21
+ from dataclasses import dataclass, field
22
+
23
+
24
+ @dataclass
25
+ class TrainingSpec:
26
+ """Local-only training.
27
+
28
+ MerLin's remote processor cannot backpropagate -- the documentation is
29
+ explicit: *"No gradient can flow through the MerLin processor."* So a spec
30
+ carrying ``training`` is refused on a remote device and only runs on the
31
+ local simulator.
32
+ """
33
+
34
+ targets: object
35
+ epochs: int = 10
36
+ learning_rate: float = 0.01
37
+ loss: str = 'mse'
38
+
39
+
40
+ @dataclass
41
+ class MerlinJobSpec:
42
+ """One MerLin job: one model, one input batch, one machine.
43
+
44
+ @param model: the torch Module to run. Must contain exactly one quantum
45
+ layer -- see the single-machine rules in
46
+ ``model/device/merlin_remote_device.py``.
47
+ @param inputs: the input batch, shape ``[batch, input_size]``. Anything
48
+ ``torch.as_tensor`` accepts is fine (list, ndarray, tensor).
49
+ @param shots: sample count. ``None`` or ``0`` means exact probabilities --
50
+ MerLin does not invent a default, and neither do we.
51
+ @param sampling_method: e.g. ``'multinomial'``; ``None`` leaves MerLin's
52
+ default alone.
53
+ @param training: present only for local training runs.
54
+ @param metadata: free-form, copied into the job result for the handler's own
55
+ post_processing().
56
+ """
57
+
58
+ model: object
59
+ inputs: object
60
+ shots: int = None
61
+ sampling_method: str = None
62
+ training: TrainingSpec = None
63
+ metadata: dict = field(default_factory=dict)
64
+
65
+
66
+ SPEC_HELP = (
67
+ 'processing() must return a MerlinJobSpec, or a torch.nn.Module when the '
68
+ 'input batch is in invocation_input["inputs"].')
69
+
70
+
71
+ def coerce(prepared, invocation_input: dict = None) -> MerlinJobSpec:
72
+ """Turn what ``processing()`` returned into a MerlinJobSpec.
73
+
74
+ A bare Module is accepted because the common case -- one layer, data already
75
+ in the request -- should not need the import. The data then has to be in
76
+ ``invocation_input["inputs"]``; anything else is rejected here rather than
77
+ surfacing later as a shape error inside torch.
78
+ """
79
+ if isinstance(prepared, MerlinJobSpec):
80
+ return prepared
81
+
82
+ if hasattr(prepared, 'forward') and callable(prepared):
83
+ inputs = (invocation_input or {}).get('inputs')
84
+ if inputs is None:
85
+ raise ValueError(
86
+ 'processing() returned a model but no input batch: put it '
87
+ 'in invocation_input["inputs"], or return a MerlinJobSpec '
88
+ 'with inputs=... instead.')
89
+ return MerlinJobSpec(model=prepared, inputs=inputs,
90
+ shots=(invocation_input or {}).get('shots'))
91
+
92
+ raise ValueError(
93
+ f'processing() returned {type(prepared).__name__}. {SPEC_HELP}')