quapp-merlin 0.0.1.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quapp_merlin/__init__.py +8 -0
- quapp_merlin/component/__init__.py +0 -0
- quapp_merlin/component/backend/__init__.py +0 -0
- quapp_merlin/component/backend/merlin_invocation.py +82 -0
- quapp_merlin/data/__init__.py +0 -0
- quapp_merlin/data/merlin_job_spec.py +93 -0
- quapp_merlin/error_rules.py +190 -0
- quapp_merlin/factory/__init__.py +0 -0
- quapp_merlin/factory/merlin_device_factory.py +42 -0
- quapp_merlin/factory/merlin_handler_factory.py +35 -0
- quapp_merlin/factory/merlin_provider_factory.py +36 -0
- quapp_merlin/handler/__init__.py +0 -0
- quapp_merlin/handler/invocation_handler.py +59 -0
- quapp_merlin/model/__init__.py +0 -0
- quapp_merlin/model/device/__init__.py +0 -0
- quapp_merlin/model/device/merlin_device.py +160 -0
- quapp_merlin/model/device/merlin_local_device.py +195 -0
- quapp_merlin/model/device/merlin_remote_device.py +164 -0
- quapp_merlin/model/provider/__init__.py +0 -0
- quapp_merlin/model/provider/quandela_merlin_provider.py +57 -0
- quapp_merlin/model/provider/quapp_merlin_provider.py +31 -0
- quapp_merlin/util/__init__.py +0 -0
- quapp_merlin/util/single_machine.py +135 -0
- quapp_merlin/util/tensors.py +127 -0
- quapp_merlin-0.0.1.dev1.dist-info/METADATA +138 -0
- quapp_merlin-0.0.1.dev1.dist-info/RECORD +29 -0
- quapp_merlin-0.0.1.dev1.dist-info/WHEEL +5 -0
- quapp_merlin-0.0.1.dev1.dist-info/licenses/LICENSE +8 -0
- quapp_merlin-0.0.1.dev1.dist-info/top_level.txt +1 -0
quapp_merlin/__init__.py
ADDED
|
@@ -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']
|
|
File without changes
|
|
File without changes
|
|
@@ -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}')
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# error_rules.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""MerLin-specific error classification rules.
|
|
6
|
+
|
|
7
|
+
Registered into ``quapp_common.util.error_classifier`` when ``quapp_merlin`` is
|
|
8
|
+
imported, so ``quapp-common`` never has to depend on MerLin, torch or Perceval.
|
|
9
|
+
|
|
10
|
+
Matching is done on the message text or on the class **NAME as a string**, never
|
|
11
|
+
``isinstance``, and nothing is imported from ``merlin`` / ``torch`` / ``perceval``:
|
|
12
|
+
the classifier has to work even when the SDKs are absent, and importing torch
|
|
13
|
+
here would make every ``import quapp_merlin`` pay for it.
|
|
14
|
+
|
|
15
|
+
The first group keys off messages **this lib itself** raises. They are the rules
|
|
16
|
+
that matter most, because every single-machine constraint is enforced by raising
|
|
17
|
+
one of them -- without a rule they would all land on a generic
|
|
18
|
+
``PREPROCESSING_FAILED`` and tell the user nothing about what to change:
|
|
19
|
+
|
|
20
|
+
util/single_machine.py 'Handler attached its own quantum backend'
|
|
21
|
+
'has N quantum layers'
|
|
22
|
+
'has no quantum layer'
|
|
23
|
+
'Input batch has N rows'
|
|
24
|
+
data/merlin_job_spec.py 'processing() returned ...'
|
|
25
|
+
model/device/merlin_remote_device.py
|
|
26
|
+
'Training cannot run on a remote device'
|
|
27
|
+
'shots=N exceeds'
|
|
28
|
+
model/provider/quandela_merlin_provider.py
|
|
29
|
+
'No Quandela Cloud token'
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from quapp_common.enum.error_category import ErrorCategory
|
|
33
|
+
from quapp_common.util.error_classifier import Rule, register_rules
|
|
34
|
+
|
|
35
|
+
DOC_MACHINE = 'errors/merlin-single-machine'
|
|
36
|
+
DOC_SPEC = 'errors/merlin-job-spec'
|
|
37
|
+
DOC_DEVICE = 'errors/merlin-device'
|
|
38
|
+
DOC_AUTH = 'errors/quandela-auth'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _message(exception) -> str:
|
|
42
|
+
return str(exception).lower()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _name(exception) -> str:
|
|
46
|
+
return type(exception).__name__
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
register_rules(
|
|
50
|
+
# ----------------- raised by quapp-merlin itself (one job, one machine)
|
|
51
|
+
Rule(match=lambda e: 'attached its own quantum backend' in _message(e),
|
|
52
|
+
category=ErrorCategory.USER_CODE,
|
|
53
|
+
code='MERLIN_FOREIGN_BACKEND',
|
|
54
|
+
resolution='The handler built its own quantum backend. A job runs '
|
|
55
|
+
'on exactly one machine, and the platform chooses it.',
|
|
56
|
+
steps=('Remove the MerlinProcessor / RemoteProcessor / Session '
|
|
57
|
+
'from the model returned by processing()',
|
|
58
|
+
'Pick the machine through the device of the job instead'),
|
|
59
|
+
doc_slug=DOC_MACHINE),
|
|
60
|
+
|
|
61
|
+
Rule(match=lambda e: 'quantum layers' in _message(e)
|
|
62
|
+
and 'one invocation runs one' in _message(e),
|
|
63
|
+
category=ErrorCategory.USER_CODE,
|
|
64
|
+
code='MERLIN_MULTI_LAYER_NOT_ALLOWED',
|
|
65
|
+
resolution='The model holds more than one quantum layer; a single '
|
|
66
|
+
'invocation runs one.',
|
|
67
|
+
steps=('Split the layers across separate invocations',
|
|
68
|
+
'Or run the whole model on the local simulator'),
|
|
69
|
+
doc_slug=DOC_MACHINE),
|
|
70
|
+
|
|
71
|
+
Rule(match=lambda e: 'no quantum layer' in _message(e),
|
|
72
|
+
category=ErrorCategory.USER_CODE,
|
|
73
|
+
code='MERLIN_NO_QUANTUM_LAYER',
|
|
74
|
+
resolution='The model has no quantum layer, so there is nothing '
|
|
75
|
+
'for a quantum device to run.',
|
|
76
|
+
steps=('Add a merlin.QuantumLayer to the model',
|
|
77
|
+
'A purely classical model does not need a quantum job'),
|
|
78
|
+
doc_slug=DOC_SPEC),
|
|
79
|
+
|
|
80
|
+
Rule(match=lambda e: 'input batch has' in _message(e)
|
|
81
|
+
and 'rows' in _message(e),
|
|
82
|
+
category=ErrorCategory.INVALID_INPUT,
|
|
83
|
+
code='MERLIN_BATCH_TOO_LARGE',
|
|
84
|
+
resolution='The input batch is large enough that MerLin would '
|
|
85
|
+
'split it into several cloud jobs.',
|
|
86
|
+
steps=('Send fewer rows per invocation',
|
|
87
|
+
'Or run the batch on the local simulator'),
|
|
88
|
+
doc_slug=DOC_MACHINE),
|
|
89
|
+
|
|
90
|
+
Rule(match=lambda e: 'processing() returned' in _message(e)
|
|
91
|
+
or 'must return a merlinjobspec' in _message(e),
|
|
92
|
+
category=ErrorCategory.USER_CODE,
|
|
93
|
+
code='MERLIN_INVALID_JOB_SPEC',
|
|
94
|
+
resolution='processing() did not return something MerLin can run.',
|
|
95
|
+
steps=('Return a MerlinJobSpec(model=..., inputs=...)',
|
|
96
|
+
'Or return a torch model and put the batch in '
|
|
97
|
+
'invocation_input["inputs"]'),
|
|
98
|
+
doc_slug=DOC_SPEC),
|
|
99
|
+
|
|
100
|
+
Rule(match=lambda e: 'training cannot run on a remote device' in _message(e),
|
|
101
|
+
category=ErrorCategory.USER_CODE,
|
|
102
|
+
code='MERLIN_TRAINING_NOT_REMOTE',
|
|
103
|
+
resolution='Training cannot run on a remote machine: no gradient '
|
|
104
|
+
'flows through the MerLin processor.',
|
|
105
|
+
steps=('Train on the local simulator',
|
|
106
|
+
'Send the trained model to the QPU for inference only'),
|
|
107
|
+
doc_slug=DOC_MACHINE),
|
|
108
|
+
|
|
109
|
+
Rule(match=lambda e: 'exceeds the' in _message(e)
|
|
110
|
+
and 'single cloud call' in _message(e),
|
|
111
|
+
category=ErrorCategory.INVALID_INPUT,
|
|
112
|
+
code='MERLIN_SHOTS_TOO_MANY',
|
|
113
|
+
resolution='The requested shot count is above what one cloud call '
|
|
114
|
+
'accepts, and would be silently clamped.',
|
|
115
|
+
steps=('Lower shots for this job',
|
|
116
|
+
'Or split the run across several invocations'),
|
|
117
|
+
doc_slug=DOC_MACHINE),
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------- Quandela Cloud auth
|
|
120
|
+
Rule(match=lambda e: 'no quandela cloud token' in _message(e),
|
|
121
|
+
category=ErrorCategory.AUTHENTICATION,
|
|
122
|
+
code='QUANDELA_TOKEN_MISSING',
|
|
123
|
+
resolution='No Quandela Cloud token is configured for this '
|
|
124
|
+
'workspace.',
|
|
125
|
+
steps=('Open Workspace -> Providers -> Quandela',
|
|
126
|
+
'Add the API token from cloud.quandela.com'),
|
|
127
|
+
doc_slug=DOC_AUTH,
|
|
128
|
+
retryable=False),
|
|
129
|
+
|
|
130
|
+
Rule(match=lambda e: 'unsupported device' in _message(e)
|
|
131
|
+
or 'unsupported provider' in _message(e),
|
|
132
|
+
category=ErrorCategory.PROVIDER_DEVICE,
|
|
133
|
+
code='MERLIN_DEVICE_UNSUPPORTED',
|
|
134
|
+
resolution="This device or provider is not supported by Quapp's "
|
|
135
|
+
'MerLin lib yet.',
|
|
136
|
+
steps=('Check the device selected for the job',
|
|
137
|
+
'quapp-merlin serves QUAO_QUANTUM_SIMULATOR and QUANDELA'),
|
|
138
|
+
doc_slug=DOC_DEVICE),
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------ torch failures
|
|
141
|
+
# A Fock space grows combinatorially with modes and photons, so the
|
|
142
|
+
# usual way to run out of memory here is an over-ambitious layer, not a
|
|
143
|
+
# platform fault -- the user is the one who can fix it.
|
|
144
|
+
Rule(match=lambda e: _name(e) == 'OutOfMemoryError'
|
|
145
|
+
or 'out of memory' in _message(e)
|
|
146
|
+
or 'cannot allocate' in _message(e),
|
|
147
|
+
category=ErrorCategory.USER_CODE,
|
|
148
|
+
code='MERLIN_OUT_OF_MEMORY',
|
|
149
|
+
resolution='The simulation ran out of memory: the state space of '
|
|
150
|
+
'this circuit is too large for the function.',
|
|
151
|
+
steps=('Use fewer modes or fewer photons',
|
|
152
|
+
'Reduce the batch size',
|
|
153
|
+
'Consider no_bunching or a smaller computation space')),
|
|
154
|
+
|
|
155
|
+
Rule(match=lambda e: _name(e) == 'RuntimeError'
|
|
156
|
+
and ('shapes cannot be multiplied' in _message(e)
|
|
157
|
+
or 'size mismatch' in _message(e)
|
|
158
|
+
or 'must match the size' in _message(e)
|
|
159
|
+
or 'expected input' in _message(e)),
|
|
160
|
+
category=ErrorCategory.USER_CODE,
|
|
161
|
+
code='MERLIN_SHAPE_MISMATCH',
|
|
162
|
+
resolution='The input tensor does not match what the model '
|
|
163
|
+
'expects.',
|
|
164
|
+
steps=('Check input_size of the quantum layer against the width '
|
|
165
|
+
'of your input batch',
|
|
166
|
+
'inputs must have shape [batch, input_size]'),
|
|
167
|
+
doc_slug=DOC_SPEC),
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------- remote execution failures
|
|
170
|
+
Rule(match=lambda e: _name(e) in {'TimeoutError', 'FuturesTimeoutError'}
|
|
171
|
+
or 'timed out' in _message(e),
|
|
172
|
+
category=ErrorCategory.TIMEOUT,
|
|
173
|
+
code='MERLIN_REMOTE_TIMEOUT',
|
|
174
|
+
resolution='The remote machine did not return a result in time.',
|
|
175
|
+
steps=('The QPU queue may be long -- try again later',
|
|
176
|
+
'Reduce shots or batch size to shorten the run'),
|
|
177
|
+
retryable=True,
|
|
178
|
+
retry_after=300),
|
|
179
|
+
|
|
180
|
+
# Placed last: matching is by NAME, so only these exact names land here.
|
|
181
|
+
Rule(match=lambda e: _name(e) in {'PercevalError', 'ProcessorError',
|
|
182
|
+
'RPCError', 'CircuitError'},
|
|
183
|
+
category=ErrorCategory.SDK_RUNTIME,
|
|
184
|
+
code='MERLIN_SDK_ERROR',
|
|
185
|
+
resolution='Perceval failed while running the circuit behind '
|
|
186
|
+
'MerLin. See the stack trace below.',
|
|
187
|
+
steps=('Read the rootCause to see which step was rejected',
|
|
188
|
+
'Check the mode count and photon count the platform '
|
|
189
|
+
'supports')),
|
|
190
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# merlin_device_factory.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
from quapp_common.config.logging_config import logger
|
|
6
|
+
from quapp_common.enum.provider_tag import ProviderTag
|
|
7
|
+
from quapp_common.enum.sdk import Sdk
|
|
8
|
+
from quapp_common.factory.device_factory import DeviceFactory
|
|
9
|
+
from quapp_common.model.provider.provider import Provider
|
|
10
|
+
|
|
11
|
+
from ..model.device.merlin_local_device import MerlinLocalDevice
|
|
12
|
+
from ..model.device.merlin_remote_device import MerlinRemoteDevice
|
|
13
|
+
|
|
14
|
+
logger = logger.bind(context='MerlinDeviceFactory')
|
|
15
|
+
|
|
16
|
+
_MERLIN_SDK = getattr(Sdk, 'MERLIN', None)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MerlinDeviceFactory(DeviceFactory):
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def create_device(provider: Provider, device_specification: str,
|
|
23
|
+
authentication: dict, sdk: Sdk):
|
|
24
|
+
logger.debug(f'Create Merlin device: {device_specification}')
|
|
25
|
+
|
|
26
|
+
if _MERLIN_SDK is not None and not _MERLIN_SDK.__eq__(sdk):
|
|
27
|
+
raise Exception('Unsupported device!')
|
|
28
|
+
|
|
29
|
+
provider_type = ProviderTag.resolve(
|
|
30
|
+
provider.get_provider_type().value)
|
|
31
|
+
|
|
32
|
+
# One device, one machine: the remote device receives the
|
|
33
|
+
# RemoteProcessor the provider just built and uses nothing else.
|
|
34
|
+
if provider_type is ProviderTag.QUANDELA:
|
|
35
|
+
logger.info('Created remote Merlin device')
|
|
36
|
+
return MerlinRemoteDevice(provider, device_specification)
|
|
37
|
+
|
|
38
|
+
if provider_type is ProviderTag.QUAO_QUANTUM_SIMULATOR:
|
|
39
|
+
logger.info('Created local Merlin device')
|
|
40
|
+
return MerlinLocalDevice(provider, device_specification)
|
|
41
|
+
|
|
42
|
+
raise Exception('Unsupported device!')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# merlin_handler_factory.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
from quapp_common.config.logging_config import job_logger
|
|
6
|
+
from quapp_common.factory.handler_factory import HandlerFactory
|
|
7
|
+
from quapp_common.handler.handler import Handler
|
|
8
|
+
|
|
9
|
+
from ..handler.invocation_handler import InvocationHandler
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MerlinHandlerFactory(HandlerFactory):
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def create_handler(event, circuit_preparation_fn,
|
|
16
|
+
post_processing_fn) -> Handler:
|
|
17
|
+
request_data = event.json()
|
|
18
|
+
|
|
19
|
+
# event.json() returns a DICT, so getattr(..., 'jobId') would always
|
|
20
|
+
# give None and the job_id would never get bound. Read it with .get().
|
|
21
|
+
logger = job_logger(request_data.get('jobId'))
|
|
22
|
+
logger.debug('Create Merlin Handler')
|
|
23
|
+
|
|
24
|
+
provider_job_id = request_data.get('providerJobId')
|
|
25
|
+
|
|
26
|
+
if provider_job_id is None:
|
|
27
|
+
logger.debug('providerJobId is None. Create InvocationHandler')
|
|
28
|
+
return InvocationHandler(
|
|
29
|
+
request_data=request_data,
|
|
30
|
+
circuit_preparation_fn=circuit_preparation_fn,
|
|
31
|
+
post_processing_fn=post_processing_fn)
|
|
32
|
+
|
|
33
|
+
raise ValueError(
|
|
34
|
+
'MerLin has no job-fetching handler: a run is synchronous and '
|
|
35
|
+
'its result is returned by the invocation that started it.')
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# merlin_provider_factory.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
from quapp_common.config.logging_config import logger
|
|
6
|
+
from quapp_common.enum.provider_tag import ProviderTag
|
|
7
|
+
from quapp_common.enum.sdk import Sdk
|
|
8
|
+
from quapp_common.factory.provider_factory import ProviderFactory
|
|
9
|
+
|
|
10
|
+
from ..model.provider.quandela_merlin_provider import QuandelaMerlinProvider
|
|
11
|
+
from ..model.provider.quapp_merlin_provider import QuappMerlinProvider
|
|
12
|
+
|
|
13
|
+
logger = logger.bind(context='MerlinProviderFactory')
|
|
14
|
+
|
|
15
|
+
_MERLIN_SDK = getattr(Sdk, 'MERLIN', None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MerlinProviderFactory(ProviderFactory):
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def create_provider(provider_type: ProviderTag, sdk: Sdk,
|
|
22
|
+
authentication: dict):
|
|
23
|
+
logger.debug(f'Create Merlin provider for {provider_type}')
|
|
24
|
+
|
|
25
|
+
if _MERLIN_SDK is not None and not _MERLIN_SDK.__eq__(sdk):
|
|
26
|
+
raise Exception('Unsupported provider!')
|
|
27
|
+
|
|
28
|
+
if provider_type is ProviderTag.QUANDELA:
|
|
29
|
+
logger.debug('Create Quandela Merlin Provider')
|
|
30
|
+
return QuandelaMerlinProvider(authentication=authentication)
|
|
31
|
+
|
|
32
|
+
if provider_type is ProviderTag.QUAO_QUANTUM_SIMULATOR:
|
|
33
|
+
logger.debug('Create Quapp Merlin Provider')
|
|
34
|
+
return QuappMerlinProvider(authentication=authentication)
|
|
35
|
+
|
|
36
|
+
raise Exception('Unsupported provider!')
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# invocation_handler.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""Entry point for a MerLin invocation.
|
|
6
|
+
|
|
7
|
+
One thing differs from the other libs: the handler's ``processing()`` returns a
|
|
8
|
+
model, not a circuit, so it is wrapped here to produce a validated
|
|
9
|
+
``MerlinJobSpec`` before anything else touches it.
|
|
10
|
+
|
|
11
|
+
The wrapper runs inside ``Invocation.__prepare_circuit``, which means inside
|
|
12
|
+
``with phase(PREPROCESSING)`` -- so a malformed return value is reported as a
|
|
13
|
+
preprocessing error belonging to the user, which is exactly what it is.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from quapp_common.data.request.invocation_request import InvocationRequest
|
|
17
|
+
from quapp_common.handler.handler import Handler
|
|
18
|
+
|
|
19
|
+
from ..component.backend.merlin_invocation import MerlinInvocation
|
|
20
|
+
from ..data.merlin_job_spec import coerce
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InvocationHandler(Handler):
|
|
24
|
+
def __init__(self, request_data: dict, circuit_preparation_fn,
|
|
25
|
+
post_processing_fn):
|
|
26
|
+
super().__init__(request_data, post_processing_fn)
|
|
27
|
+
self.circuit_preparation_fn = circuit_preparation_fn
|
|
28
|
+
|
|
29
|
+
def _prepare_spec(self):
|
|
30
|
+
"""``processing()``, with its return value normalised to a job spec."""
|
|
31
|
+
|
|
32
|
+
def prepare(invocation_input):
|
|
33
|
+
prepared = self.circuit_preparation_fn(invocation_input)
|
|
34
|
+
spec = coerce(prepared, invocation_input)
|
|
35
|
+
self.logger.debug(
|
|
36
|
+
f'Job spec ready: model={type(spec.model).__name__}, '
|
|
37
|
+
f'shots={spec.shots}, training={spec.training is not None}')
|
|
38
|
+
return spec
|
|
39
|
+
|
|
40
|
+
return prepare
|
|
41
|
+
|
|
42
|
+
def handle(self):
|
|
43
|
+
self.logger.debug('Handle Invocation: start')
|
|
44
|
+
try:
|
|
45
|
+
invocation_request = InvocationRequest(self.request_data)
|
|
46
|
+
self.logger.debug(f'Invocation request created successfully: '
|
|
47
|
+
f'{invocation_request.__dict__.keys()}')
|
|
48
|
+
|
|
49
|
+
backend = MerlinInvocation(invocation_request)
|
|
50
|
+
self.logger.info('Submitting job via MerlinInvocation')
|
|
51
|
+
|
|
52
|
+
backend.submit_job(
|
|
53
|
+
circuit_preparation_fn=self._prepare_spec(),
|
|
54
|
+
post_processing_fn=self.post_processing_fn)
|
|
55
|
+
|
|
56
|
+
self.logger.debug('Job submission triggered successfully')
|
|
57
|
+
except Exception as exception:
|
|
58
|
+
self.logger.exception(f'Invocation handling failed: {exception}')
|
|
59
|
+
raise
|
|
File without changes
|
|
File without changes
|